7 Commits

Author SHA1 Message Date
252a3473b3 Merge pull request 'feat-001: add Start Menu shortcut to installer' (#3) from feat-001 into main
Reviewed-on: #3
2026-05-17 15:30:07 +00:00
df6dc2f28c feat-001: add Start Menu shortcut to installer
Some checks failed
Build Check / build (push) Has been cancelled
Build Check / build (pull_request) Has been cancelled
2026-05-17 10:26:14 -05:00
defffd0b03 Merge pull request 'bug-001' (#1) from bug-001 into main
Some checks failed
Build Check / build (push) Has been cancelled
Build and Release / build (push) Successful in 3m43s
Reviewed-on: #1
2026-05-17 14:39:24 +00:00
03ba79a8ce fix version
All checks were successful
Build Check / build (push) Successful in 3m31s
Build Check / build (pull_request) Successful in 3m33s
2026-05-17 09:33:28 -05:00
168dc9de83 bug-001: fix tray menu, add autostart and crash recovery launcher
All checks were successful
Build Check / build (push) Successful in 4m2s
2026-05-17 09:26:28 -05:00
77ddf68c8f fix upload
All checks were successful
Build and Release / build (push) Successful in 3m22s
2026-05-17 02:52:37 -05:00
c682087de8 t1
All checks were successful
Build and Release / build (push) Successful in 3m24s
2026-05-17 02:45:09 -05:00
9 changed files with 424 additions and 11 deletions

View File

@@ -16,7 +16,13 @@
"Bash(curl -s -b /tmp/yf_cookies2.txt -A 'Mozilla/5.0 \\(Windows NT 10.0; Win64; x64\\) AppleWebKit/537.36 \\(KHTML, like Gecko\\) Chrome/120.0.0.0 Safari/537.36' 'https://query1.finance.yahoo.com/v10/finance/quoteSummary/AAPL?modules=cashflowStatementHistory,balanceSheetHistory,calendarEvents&crumb=t5.A45X0LQC')",
"Bash(curl -s -b /tmp/yf_cookies2.txt -A 'Mozilla/5.0 \\(Windows NT 10.0; Win64; x64\\) AppleWebKit/537.36 \\(KHTML, like Gecko\\) Chrome/120.0.0.0 Safari/537.36' 'https://query1.finance.yahoo.com/v10/finance/quoteSummary/AAPL?modules=financialData,recommendationTrend&crumb=t5.A45X0LQC')",
"Bash(curl -s -b /tmp/yf_cookies2.txt -A 'Mozilla/5.0 \\(Windows NT 10.0; Win64; x64\\) AppleWebKit/537.36 \\(KHTML, like Gecko\\) Chrome/120.0.0.0 Safari/537.36' 'https://query1.finance.yahoo.com/v10/finance/quoteSummary/AAPL?modules=incomeStatementHistoryQuarterly&crumb=t5.A45X0LQC')",
"Bash(cargo doc *)"
"Bash(cargo doc *)",
"Bash(git checkout *)",
"Bash(git push *)",
"Bash(grep -n \"tray_icon::TrayIconEvent\\\\|tray_icon::menu::MenuEvent\\\\|set_event_handler\" ~/.cargo/registry/src/*/tray-icon-0.14*/src/lib.rs 2>/dev/null | head -20)",
"Bash(git add *)",
"Bash(git commit *)",
"Bash(git pull *)"
]
}
}

View File

@@ -0,0 +1,26 @@
name: Build Check
on:
push:
branches:
- 'bug-*'
- 'feat-*'
- 'dev'
pull_request:
branches:
- main
jobs:
build:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Build check
shell: powershell
run: cargo build --release
- name: Report success
shell: powershell
run: Write-Host "Build succeeded - safe to merge to main"

View File

@@ -16,6 +16,11 @@ jobs:
shell: powershell
run: cargo build --release
- name: Build installer
shell: powershell
run: |
cargo wix --nocapture
- name: Create Gitea Release
id: create_release
shell: powershell
@@ -38,7 +43,9 @@ jobs:
} `
-Body $body
"release_id=$($response.id)" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
$releaseId = $response.id
Write-Host "Created release ID: $releaseId"
Add-Content -Path $env:GITHUB_OUTPUT -Value "release_id=$releaseId"
- name: Upload binary asset
shell: powershell
@@ -57,4 +64,21 @@ jobs:
$uri = "https://gitea.whitlocktech.com/api/v1/repos/whitlocktech/tickr/releases/$releaseId/assets?name=tickr.exe"
& curl.exe -X POST -H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" -F "attachment=@`"$filePath`"" "$uri"
& curl.exe -X POST -H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" -F "attachment=@$filePath" "$uri"
- name: Upload installer
shell: powershell
run: |
$releaseId = "${{ steps.create_release.outputs.release_id }}"
$msiPath = (Get-ChildItem -Path "target\wix\*.msi" | Select-Object -First 1).FullName
Write-Host "MSI Path: $msiPath"
if (!(Test-Path $msiPath)) {
Write-Error "MSI not found"
exit 1
}
$uri = "https://gitea.whitlocktech.com/api/v1/repos/whitlocktech/tickr/releases/$releaseId/assets?name=tickr-setup.msi"
& curl.exe -X POST -H "Authorization: token ${{ secrets.RELEASE_TOKEN }}" -F "attachment=@$msiPath" "$uri"

View File

@@ -1,12 +1,18 @@
[package]
name = "tickr"
version = "0.4.0"
version = "0.5.0"
edition = "2021"
authors = ["Whitlocktech"]
license = "MIT"
[[bin]]
name = "tickr"
path = "src/main.rs"
[[bin]]
name = "tickr-launcher"
path = "src/launcher.rs"
[dependencies]
eframe = { version = "0.27", features = ["default"] }
egui = "0.27"
@@ -19,6 +25,7 @@ anyhow = "1"
toml = "0.8"
dirs = "5"
semver = "1"
winreg = "0.52"
tray-icon = "0.14"
image = { version = "0.25", default-features = false, features = ["ico"] }
@@ -30,3 +37,6 @@ strip = true
[target.'cfg(windows)'.build-dependencies]
winres = "0.1"
[package.metadata.wix]
eula = false

View File

@@ -74,12 +74,16 @@ pub struct TickrApp {
update_in_progress: bool,
update_result_rx: mpsc::Receiver<UpdateCheckResult>,
// Tray
// Tray — use our own channels fed by set_event_handler so events are
// delivered even when the window is hidden (the global receiver() only
// works reliably while the winit loop is actively processing messages).
#[allow(dead_code)]
tray_icon: Option<tray_icon::TrayIcon>,
tray_open_id: Option<tray_icon::menu::MenuId>,
tray_check_id: Option<tray_icon::menu::MenuId>,
tray_quit_id: Option<tray_icon::menu::MenuId>,
tray_ev_rx: mpsc::Receiver<tray_icon::TrayIconEvent>,
menu_ev_rx: mpsc::Receiver<tray_icon::menu::MenuEvent>,
window_visible: bool,
hide_to_tray: bool,
should_quit: bool,
@@ -108,6 +112,24 @@ impl TickrApp {
let (tray_icon, tray_open_id, tray_check_id, tray_quit_id) =
setup_tray(icon_rgba.clone(), icon_size, watchlist.tickers.len());
// Route tray events through our own sync channels + wake egui immediately.
// SyncSender is Send + Sync, satisfying the 'static closure requirement.
// (The global receiver() channel is NOT used — set_event_handler replaces it.)
let (tray_ev_tx, tray_ev_rx) = mpsc::sync_channel::<tray_icon::TrayIconEvent>(64);
let (menu_ev_tx, menu_ev_rx) = mpsc::sync_channel::<tray_icon::menu::MenuEvent>(64);
let ctx_tray = cc.egui_ctx.clone();
tray_icon::TrayIconEvent::set_event_handler(Some(move |e| {
let _ = tray_ev_tx.send(e);
ctx_tray.request_repaint();
}));
let ctx_menu = cc.egui_ctx.clone();
tray_icon::menu::MenuEvent::set_event_handler(Some(move |e| {
let _ = menu_ev_tx.send(e);
ctx_menu.request_repaint();
}));
// Spawn background check task
let (bg_cmd_tx, bg_cmd_rx) = tokio::sync::mpsc::unbounded_channel::<BgCmd>();
let (update_result_tx, update_result_rx) = mpsc::channel::<UpdateCheckResult>();
@@ -138,6 +160,8 @@ impl TickrApp {
tray_open_id,
tray_check_id,
tray_quit_id,
tray_ev_rx,
menu_ev_rx,
window_visible: start_visible,
hide_to_tray: false,
should_quit: false,
@@ -870,17 +894,16 @@ impl TickrApp {
}
fn poll_tray_events(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
use tray_icon::TrayIconEvent;
while let Ok(event) = TrayIconEvent::receiver().try_recv() {
// Drain icon-level events (single/double click → show window)
while let Ok(event) = self.tray_ev_rx.try_recv() {
match event {
TrayIconEvent::Click { .. } => self.show_window(ctx),
tray_icon::TrayIconEvent::Click { .. } => self.show_window(ctx),
_ => {}
}
}
use tray_icon::menu::MenuEvent;
while let Ok(event) = MenuEvent::receiver().try_recv() {
// Drain context-menu events
while let Ok(event) = self.menu_ev_rx.try_recv() {
if self.tray_open_id.as_ref() == Some(&event.id) {
self.show_window(ctx);
} else if self.tray_check_id.as_ref() == Some(&event.id) {

49
src/launcher.rs Normal file
View File

@@ -0,0 +1,49 @@
// tickr-launcher — thin crash-recovery wrapper for tickr.exe
//
// Spawns tickr.exe from the same directory, watches it, and relaunches on
// non-zero exit codes (crashes). A clean exit (code 0) stops the loop so the
// user can quit intentionally without the launcher interfering.
use std::process::Command;
use std::thread;
use std::time::Duration;
fn main() {
let exe_path = std::env::current_exe()
.expect("tickr-launcher: cannot resolve own path");
let exe_dir = exe_path
.parent()
.expect("tickr-launcher: cannot resolve exe directory");
let tickr = exe_dir.join("tickr.exe");
loop {
let mut child = match Command::new(&tickr).spawn() {
Ok(c) => c,
Err(e) => {
eprintln!("tickr-launcher: failed to spawn tickr.exe: {e}");
thread::sleep(Duration::from_secs(3));
continue;
}
};
let status = match child.wait() {
Ok(s) => s,
Err(e) => {
eprintln!("tickr-launcher: wait() error: {e}");
thread::sleep(Duration::from_secs(3));
continue;
}
};
match status.code() {
Some(0) => break, // intentional clean exit — do not relaunch
code => {
eprintln!(
"tickr-launcher: tickr.exe exited with code {:?}, relaunching in 3 s…",
code
);
thread::sleep(Duration::from_secs(3));
}
}
}
}

View File

@@ -14,7 +14,41 @@ mod watchlist;
pub use version::VERSION;
/// Write `HKCU\...\Run\Tickr` pointing to `tickr-launcher.exe` on first launch.
/// If the key already exists (any value) it is left untouched.
fn ensure_autostart() {
use winreg::enums::{HKEY_CURRENT_USER, KEY_READ, KEY_WRITE};
use winreg::RegKey;
let exe_path = match std::env::current_exe() {
Ok(p) => p,
Err(_) => return,
};
let exe_dir = match exe_path.parent() {
Some(d) => d,
None => return,
};
// The launcher watches tickr.exe; autostart should invoke the launcher.
let launcher = exe_dir.join("tickr-launcher.exe");
let launcher_str = launcher.to_string_lossy().into_owned();
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
let run_key = match hkcu.open_subkey_with_flags(
"Software\\Microsoft\\Windows\\CurrentVersion\\Run",
KEY_READ | KEY_WRITE,
) {
Ok(k) => k,
Err(_) => return,
};
// Only add if not already present
if run_key.get_value::<String, _>("Tickr").is_err() {
let _ = run_key.set_value("Tickr", &launcher_str);
}
}
fn main() -> eframe::Result<()> {
ensure_autostart();
let config = config::ensure_exists();
let (rgba, size) = icon::make_rgba();

BIN
wix/License.rtf Normal file

Binary file not shown.

241
wix/main.wxs Normal file
View File

@@ -0,0 +1,241 @@
<?xml version='1.0' encoding='windows-1252'?>
<!--
Copyright (C) 2017 Christopher R. Field.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!--
The "cargo wix" subcommand provides a variety of predefined variables available
for customization of this template. The values for each variable are set at
installer creation time. The following variables are available:
TargetTriple = The rustc target triple name.
TargetEnv = The rustc target environment. This is typically either
"msvc" or "gnu" depending on the toolchain downloaded and
installed.
TargetVendor = The rustc target vendor. This is typically "pc", but Rust
does support other vendors, like "uwp".
CargoTargetBinDir = The complete path to the directory containing the
binaries (exes) to include. The default would be
"target\release\". If an explicit rustc target triple is
used, i.e. cross-compiling, then the default path would
be "target\<CARGO_TARGET>\<CARGO_PROFILE>",
where "<CARGO_TARGET>" is replaced with the "CargoTarget"
variable value and "<CARGO_PROFILE>" is replaced with the
value from the "CargoProfile" variable. This can also
be overridden manually with the "target-bin-dir" flag.
CargoTargetDir = The path to the directory for the build artifacts, i.e.
"target".
CargoProfile = The cargo profile used to build the binaries
(usually "debug" or "release").
Version = The version for the installer. The default is the
"Major.Minor.Fix" semantic versioning number of the Rust
package.
-->
<!--
Please do not remove these pre-processor If-Else blocks. These are used with
the `cargo wix` subcommand to automatically determine the installation
destination for 32-bit versus 64-bit installers. Removal of these lines will
cause installation errors.
-->
<?if $(sys.BUILDARCH) = x64 or $(sys.BUILDARCH) = arm64 ?>
<?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
<?else ?>
<?define PlatformProgramFilesFolder = "ProgramFilesFolder" ?>
<?endif ?>
<Wix xmlns='http://schemas.microsoft.com/wix/2006/wi'>
<Product
Id='*'
Name='tickr'
UpgradeCode='52EC33B1-1203-4C27-89BC-82E53707AD5B'
Manufacturer='Whitlocktech'
Language='1033'
Codepage='1252'
Version='$(var.Version)'>
<Package Id='*'
Keywords='Installer'
Manufacturer='Whitlocktech'
InstallerVersion='450'
Languages='1033'
Compressed='yes'
InstallScope='perMachine'
SummaryCodepage='1252'
/>
<MajorUpgrade
Schedule='afterInstallInitialize'
DowngradeErrorMessage='A newer version of [ProductName] is already installed. Setup will now exit.'/>
<Media Id='1' Cabinet='media1.cab' EmbedCab='yes' DiskPrompt='CD-ROM #1'/>
<Property Id='DiskPrompt' Value='tickr Installation'/>
<Directory Id='TARGETDIR' Name='SourceDir'>
<Directory Id='ProgramMenuFolder'>
<Directory Id='ApplicationProgramsFolder' Name='Tickr'/>
</Directory>
<Directory Id='$(var.PlatformProgramFilesFolder)' Name='PFiles'>
<Directory Id='APPLICATIONFOLDER' Name='tickr'>
<!--
Disabling the license sidecar file in the installer is a two step process:
1. Comment out or remove the `Component` tag along with its contents.
2. Comment out or remove the `ComponentRef` tag with the "License" Id
attribute value further down in this file.
-->
<Component Id='License' Guid='*'>
<File Id='LicenseFile'
DiskId='1'
Source='wix\License.rtf'
KeyPath='yes'/>
</Component>
<Directory Id='Bin' Name='bin'>
<Component Id='Path' Guid='0F04AAC4-3D25-4777-8FBA-77C25BDB274B' KeyPath='yes'>
<Environment
Id='PATH'
Name='PATH'
Value='[Bin]'
Permanent='no'
Part='last'
Action='set'
System='yes'/>
</Component>
<Component Id='binary0' Guid='*'>
<File
Id='exe0'
Name='tickr.exe'
DiskId='1'
Source='$(var.CargoTargetBinDir)\tickr.exe'
KeyPath='yes'/>
</Component>
<Component Id='ApplicationShortcut' Guid='*'>
<Shortcut Id='ApplicationStartMenuShortcut'
Name='Tickr'
Description='Tickr Stock Ticker'
Target='[Bin]tickr-launcher.exe'
WorkingDirectory='Bin'/>
<RemoveFolder Id='CleanUpShortcut' Directory='ApplicationProgramsFolder' On='uninstall'/>
<RegistryValue Root='HKCU'
Key='Software\Whitlocktech\Tickr'
Name='installed'
Type='integer'
Value='1'
KeyPath='yes'/>
</Component>
</Directory>
</Directory>
</Directory>
</Directory>
<Feature
Id='Binaries'
Title='Application'
Description='Installs all binaries and the license.'
Level='1'
ConfigurableDirectory='APPLICATIONFOLDER'
AllowAdvertise='no'
Display='expand'
Absent='disallow'>
<!--
Comment out or remove the following `ComponentRef` tag to remove
the license sidecar file from the installer.
-->
<ComponentRef Id='License'/>
<ComponentRef Id='binary0'/>
<ComponentRef Id='ApplicationShortcut'/>
<Feature
Id='Environment'
Title='PATH Environment Variable'
Description='Add the install location of the [ProductName] executable to the PATH system environment variable. This allows the [ProductName] executable to be called from any location.'
Level='1'
Absent='allow'>
<ComponentRef Id='Path'/>
</Feature>
</Feature>
<SetProperty Id='ARPINSTALLLOCATION' Value='[APPLICATIONFOLDER]' After='CostFinalize'/>
<!--
Uncomment the following `Icon` and `Property` tags to change the product icon.
The product icon is the graphic that appears in the Add/Remove
Programs control panel for the application.
-->
<!--<Icon Id='ProductICO' SourceFile='wix\Product.ico'/>-->
<!--<Property Id='ARPPRODUCTICON' Value='ProductICO' />-->
<!--
Adding a URL to Add/Remove Programs control panel listing for the
application is a two step process:
1. Uncomment the following `Property` tag with the "ARPHELPLINK" Id
attribute value.
2. Change the value for `Value` attribute of the following
`Property` tag to a valid URL.
-->
<!--<Property Id='ARPHELPLINK' Value='ChangeMe'/>-->
<UI>
<UIRef Id='WixUI_FeatureTree'/>
<!--
Disabling the EULA dialog in the installer is a two step process:
1. Uncomment the following two `Publish` tags
2. Comment out or remove the `<WiXVariable Id='WixUILicenseRtf'...` tag further down
-->
<!--<Publish Dialog='WelcomeDlg' Control='Next' Event='NewDialog' Value='CustomizeDlg' Order='99'>1</Publish>-->
<!--<Publish Dialog='CustomizeDlg' Control='Back' Event='NewDialog' Value='WelcomeDlg' Order='99'>1</Publish>-->
</UI>
<!--
Disabling the EULA dialog in the installer requires commenting out
or removing the following `WixVariable` tag
-->
<WixVariable Id='WixUILicenseRtf' Value='wix\License.rtf'/>
<!--
Uncomment the next `WixVariable` tag to customize the installer's
Graphical User Interface (GUI) and add a custom banner image across
the top of each screen. See the WiX Toolset documentation for details
about customization.
The banner BMP dimensions are 493 x 58 pixels.
-->
<!--<WixVariable Id='WixUIBannerBmp' Value='wix\Banner.bmp'/>-->
<!--
Uncomment the next `WixVariable` tag to customize the installer's
Graphical User Interface (GUI) and add a custom image to the first
dialog, or screen. See the WiX Toolset documentation for details about
customization.
The dialog BMP dimensions are 493 x 312 pixels.
-->
<!--<WixVariable Id='WixUIDialogBmp' Value='wix\Dialog.bmp'/>-->
</Product>
</Wix>