Compare commits
3 Commits
77ddf68c8f
...
defffd0b03
| Author | SHA1 | Date | |
|---|---|---|---|
| defffd0b03 | |||
| 03ba79a8ce | |||
| 168dc9de83 |
@@ -16,7 +16,12 @@
|
||||
"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 *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
26
.gitea/workflows/build-check.yml
Normal file
26
.gitea/workflows/build-check.yml
Normal 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"
|
||||
@@ -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
|
||||
@@ -59,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"
|
||||
12
Cargo.toml
12
Cargo.toml
@@ -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
|
||||
37
src/app.rs
37
src/app.rs
@@ -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
49
src/launcher.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
34
src/main.rs
34
src/main.rs
@@ -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
BIN
wix/License.rtf
Normal file
Binary file not shown.
223
wix/main.wxs
Normal file
223
wix/main.wxs
Normal file
@@ -0,0 +1,223 @@
|
||||
<?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='$(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>
|
||||
</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'/>
|
||||
|
||||
<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>
|
||||
Reference in New Issue
Block a user