Compare commits
110 Commits
Author | SHA1 | Date | |
---|---|---|---|
365d7ec37b | |||
0368d764c6 | |||
fdc2fdc41d | |||
65e6dcf980 | |||
09e21e297c | |||
7ce6e3d10b | |||
002c5ce1d7 | |||
70f3cc749e | |||
100d25773c | |||
ebe5dcf404 | |||
f41741866a | |||
dc8f832840 | |||
8064eaa6e2 | |||
416fdc7be4 | |||
72721abb22 | |||
2f2eefddce | |||
5229b3b18a | |||
d66d60ee49 | |||
2990660e61 | |||
ca1b76302d | |||
93127aeca3 | |||
d393677f00 | |||
9b8932aca6 | |||
13ba2b6dee | |||
732c2b0240 | |||
0eb4e494ff | |||
ce22b8347c | |||
62c2c06904 | |||
12649c0f60 | |||
a63091b320 | |||
faf7ca45e0 | |||
3769428fdf | |||
f2ab61b389 | |||
8c29d40616 | |||
31c920de8a | |||
6cf1aac9de | |||
3f7f7d7b21 | |||
2c763ce6ea | |||
e68c898700 | |||
607e50294a | |||
05cd390554 | |||
c0b5c2ee29 | |||
f5b34aa89c | |||
b813753968 | |||
0c2b5c3e8b | |||
c7cf3bff16 | |||
6eece49afc | |||
dd67bdf8a9 | |||
92a9d57c05 | |||
ede8e1e11c | |||
c8ea6ee9f8 | |||
82ce8e25d9 | |||
a27fda25f5 | |||
278ee7a581 | |||
1b031229a7 | |||
e86f2d01f6 | |||
07ed90d4fc | |||
65e8c0636a | |||
2e15449d9a | |||
8e9c2c8a7d | |||
9b539d48ab | |||
aae01bff20 | |||
ef882604af | |||
eb8fa44757 | |||
6063fac699 | |||
6f8b418e80 | |||
239d110f58 | |||
b24c0fad91 | |||
00a89467fd | |||
9c3cadd7dc | |||
43bec02a2c | |||
a7746d962c | |||
0727ff31c3 | |||
482d8ebed2 | |||
df4182b32f | |||
ef030f9cd1 | |||
e484405684 | |||
4d5abd5489 | |||
cff7288c9e | |||
b1f413660f | |||
1a0ad68ad2 | |||
556b8cb7d8 | |||
563e0277aa | |||
4e00211c02 | |||
e61d0394c3 | |||
af77a60355 | |||
80cb28dc10 | |||
2968bbe325 | |||
15bf470616 | |||
19d201ecf4 | |||
cb7bfb36e8 | |||
9c16367299 | |||
68620696fc | |||
43067c0efe | |||
4773bdae87 | |||
ea7450bb8b | |||
50f7bd809a | |||
1de8cde4cf | |||
7ee783edeb | |||
9c1ac5f7f3 | |||
dca451c996 | |||
61cdfffc0d | |||
5851aec8b3 | |||
8b30abc8b6 | |||
9efa0b110d | |||
c1278a81ee | |||
fa0c32297c | |||
c6f97648c3 | |||
ec8aa3c00a | |||
de95c4284a |
5
.dir-locals.el
Normal file
5
.dir-locals.el
Normal file
@ -0,0 +1,5 @@
|
||||
;;; Directory Local Variables -*- no-byte-compile: t -*-
|
||||
;;; For more information see (info "(emacs) Directory Variables")
|
||||
|
||||
((nil . ((vc-prepare-patches-separately . nil)
|
||||
(vc-default-patch-addressee . "billy@wolfe.casa"))))
|
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
**/__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
325
I38.md
Normal file
325
I38.md
Normal file
@ -0,0 +1,325 @@
|
||||
# Welcome to I38 - Accessible i3 Window Manager
|
||||
|
||||
> **Note:** This help guide has been tailored to your specific configuration. You've chosen **BROWSER** as your web browser, **MODKEY** as your mod key, and you're using the **SCREENREADER** screen reader.
|
||||
|
||||
## Introduction to I38
|
||||
|
||||
I38 is a configuration for the i3 window manager that makes it more accessible for blind people. It features audio feedback, screen reader integration, and keyboard shortcuts designed for non-visual navigation.
|
||||
|
||||
Unlike traditional desktop environments like GNOME or MATE, i3 is a tiling window manager, which means windows are arranged in a non-overlapping layout. This can be more efficient to navigate by keyboard, as windows are organized in a predictable structure.
|
||||
|
||||
### Coming from GNOME or MATE?
|
||||
|
||||
If you're transitioning from GNOME or MATE, here are some key differences to understand:
|
||||
|
||||
- **Window Management**: In GNOME/MATE, windows can overlap freely and are typically manipulated with a mouse. In i3/I38, windows tile automatically and are primarily controlled with keyboard shortcuts.
|
||||
- **Panels and Indicators**: Instead of persistent panels with menus and indicators, I38 uses keyboard shortcuts to access functionality.
|
||||
- **Workspace Navigation**: While GNOME/MATE have workspaces that you can switch between, I38's workspaces are more central to the workflow and are accessed via dedicated keyboard shortcuts.
|
||||
- **Application Launching**: Rather than using a start menu or activities overview, I38 provides keyboard shortcuts for launching applications.
|
||||
|
||||
I38 has been configured to make this transition easier by providing a tabbed layout (similar to browser tabs) and shortcuts that may feel somewhat familiar.
|
||||
|
||||
|
||||
## Basic Concepts
|
||||
|
||||
### Workspaces
|
||||
|
||||
Workspaces act like virtual desktops, allowing you to organize applications. You have 10 workspaces available.
|
||||
|
||||
- Switch to workspace: `Control` + `F1` through `F10`
|
||||
- Move window to workspace: `Control` + `Shift` + `F1` through `F10`
|
||||
|
||||
*GNOME/MATE comparison:* Similar to workspaces in GNOME/MATE, but with dedicated keyboard shortcuts rather than overview modes or workspace switchers.
|
||||
|
||||
### Window Management
|
||||
|
||||
Windows in I38 are arranged in a tabbed layout by default, which means windows take up the entire screen and you can switch between them like browser tabs.
|
||||
|
||||
- Switch between windows: `Alt` + `Tab` (next) or `Alt` + `Shift` + `Tab` (previous)
|
||||
- Launch terminal: `MODKEY` + `Return`
|
||||
- Close window: `MODKEY` + `F4`
|
||||
- Toggle fullscreen: `MODKEY` + `BackSpace`
|
||||
- List windows in current workspace: `RATPOISONKEY` then `'` (apostrophe)
|
||||
|
||||
*GNOME/MATE comparison:* Alt+Tab works similarly to GNOME/MATE, but window placement is automatic rather than manual.
|
||||
|
||||
|
||||
## Modes in I38
|
||||
|
||||
### Default Mode
|
||||
|
||||
This is the standard mode for working with applications. Most commands start with your mod key (`MODKEY`).
|
||||
|
||||
### Ratpoison Mode
|
||||
|
||||
Ratpoison mode allows quick access to common actions using shorter key combinations. To enter Ratpoison mode, press `RATPOISONKEY`. After pressing this key, you can execute commands with single keystrokes.
|
||||
|
||||
Common Ratpoison mode commands:
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `c` | Launch a terminal |
|
||||
| `e` | Open text editor (TEXTEDITOR) |
|
||||
| `w` | Launch web browser (BROWSER) |
|
||||
| `k` | Kill (close) the current window |
|
||||
| `?` | Show I38 help |
|
||||
| `Escape` or `Control` + `g` | Exit Ratpoison mode without taking action |
|
||||
| `Shift` + `c` | Restart Cthulhu screen reader |
|
||||
| `Shift` + `o` | Restart Orca screen reader |
|
||||
| `Shift` + `t` | Toggle screen reader |
|
||||
| `Control` + `;` | Reload I38 configuration |
|
||||
| `Control` + `q` | Exit i3 (log out) |
|
||||
| `!` | Open run dialog |
|
||||
| `Alt` + `b` | Check battery status |
|
||||
| `g` | Check game controller status |
|
||||
|
||||
*GNOME/MATE comparison:* This mode has no direct equivalent in GNOME/MATE. Think of it as a command palette or quick launcher activated by a single key.
|
||||
|
||||
### Bypass Mode
|
||||
|
||||
Bypass mode passes all keys directly to the application, which is useful for applications that need many keyboard shortcuts. To enter bypass mode, press `MODKEY` + `Shift` + `BackSpace`. Use the same key combination to exit bypass mode.
|
||||
|
||||
*GNOME/MATE comparison:* In GNOME/MATE, applications always receive keyboard input directly. Bypass mode simulates this behavior within i3.
|
||||
|
||||
## Panel Mode
|
||||
|
||||
Panel Mode provides quick access to information displays and utility panels. To enter Panel Mode, press `Alt` + `Control` + `Tab`. A distinctive sound will play when Panel Mode is active.
|
||||
|
||||
In Panel Mode, single keypresses launch different information panels:
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `w` | Display weather information |
|
||||
| `Shift` + `w` | Open Magic Wormhole file transfer GUI |
|
||||
| `s` | Show system information |
|
||||
| `r` | Open reminder panel |
|
||||
| `n` | Launch notes application |
|
||||
| `b` | Open bluetooth. *requires blueman be installed at the time of your i3 config generation* |
|
||||
| `Escape` or `Control` + `g` | Exit Panel Mode without taking action |
|
||||
|
||||
Just like Ratpoison Mode, Panel Mode automatically returns you to Default Mode after a selection is made or when you press Escape/Control+g to cancel.
|
||||
|
||||
*GNOME/MATE comparison:* Panel Mode replaces the persistent system tray and status indicators used in GNOME/MATE. Instead of having always-visible panels with clickable icons, I38 provides keyboard shortcuts to access this information on demand.
|
||||
|
||||
### System Information Panel
|
||||
|
||||
The system information panel (`s` key in Panel Mode) displays vital system statistics such as:
|
||||
- CPU usage
|
||||
- Memory usage
|
||||
- Disk space
|
||||
- Network status
|
||||
- Battery level (if applicable)
|
||||
|
||||
### Weather Panel
|
||||
|
||||
The weather panel (`w` key in Panel Mode) provides current weather conditions and forecast information for your configured location.
|
||||
|
||||
### File Transfer with Magic Wormhole
|
||||
|
||||
The Magic Wormhole panel (`Shift` + `w` in Panel Mode) provides a graphical user interface to the Magic Wormhole command-line application, allowing you to securely share files with others. This offers a more accessible way to use Magic Wormhole's secure file transfer capabilities.
|
||||
|
||||
### Notes Application
|
||||
|
||||
The notes panel (`n` key in Panel Mode) provides a simple application for creating single-line notes with automatic expiration functionality:
|
||||
- Create text notes quickly
|
||||
- Set notes to automatically delete after a specified time period
|
||||
- Lock important notes to prevent automatic deletion
|
||||
- Temporary notes will expire after their set time limit
|
||||
|
||||
### Reminder Panel
|
||||
|
||||
The reminder panel (`r` key in Panel Mode) offers the same reminder functionality described in the Reminders and Notifications section, It was previously in ratpoison mode but has been moved to panel mode because it is a better fit.
|
||||
|
||||
*Note:* Because Panel Mode uses a custom implementation rather than a traditional system tray, applications that require a system tray to run may not work with I38.
|
||||
|
||||
|
||||
## Accessibility Features
|
||||
|
||||
### Screen Reader
|
||||
|
||||
I38 is configured to work with your screen reader (SCREENREADER). The screen reader will provide spoken feedback about what's happening on screen so long as there is a window. If you don't have a window open and need to change something SCREENREADER related, press Control+Alt+d to bring up the desktop, then screen reader keys should work.
|
||||
|
||||
- Toggle screen reader: `RATPOISONKEY` then `Shift` + `t`
|
||||
- Restart screen reader: `RATPOISONKEY` then `Shift` + `o` (for Orca) or `Shift` + `c` (for Cthulhu)
|
||||
- Interrupt speech: `MODKEY` + `Shift` + `F5`
|
||||
|
||||
*GNOME/MATE comparison:* GNOME uses Orca by default with its own keyboard shortcuts. I38 integrates screen readers more deeply with the window manager.
|
||||
|
||||
### Braille Display Support
|
||||
|
||||
If you've enabled braille display support during setup, I38 will start XBrlAPI automatically to provide braille output from your screen reader.
|
||||
|
||||
### OCR (Optical Character Recognition)
|
||||
|
||||
If required dependencies are installed, you can use OCR to read text from images or inaccessible applications:
|
||||
|
||||
- `MODKEY` + `F5`: Perform OCR on the entire screen and speak the content
|
||||
- In Ratpoison mode: `Print` or `MODKEY` + `r`: Perform OCR and save to clipboard
|
||||
|
||||
*GNOME/MATE comparison:* OCR features are typically not integrated into GNOME/MATE by default.
|
||||
|
||||
### Sound Effects
|
||||
|
||||
I38 provides audio feedback for many actions:
|
||||
|
||||
- Window open/close: Ascending/descending tones
|
||||
- Mode changes: Distinctive sounds for each mode
|
||||
- Workspace changes: Subtle audio cues
|
||||
- Fullscreen toggle: Special sound effect
|
||||
|
||||
This audio feedback provides non-visual confirmation of actions and state changes.
|
||||
|
||||
*GNOME/MATE comparison:* GNOME/MATE typically have fewer sound effects for window management actions.
|
||||
|
||||
## Application Menu and Running Programs
|
||||
|
||||
Access applications in multiple ways:
|
||||
|
||||
- Applications menu: `MODKEY` + `F1`
|
||||
- Run dialog (enter a command): `MODKEY` + `F2` or in Ratpoison mode, `!` (exclamation mark)
|
||||
- Common applications have dedicated shortcuts in Ratpoison mode (see table above)
|
||||
|
||||
The applications menu is organized by categories similar to traditional desktop environments.
|
||||
|
||||
*GNOME/MATE comparison:* Instead of clicking on application icons or using a start menu, I38 provides keyboard shortcuts to access applications.
|
||||
|
||||
|
||||
## Reminders and Notifications
|
||||
|
||||
I38 includes integration with the `remind` program for managing reminders:
|
||||
|
||||
- Access the reminder tool: `Alt+Control+Tab` then `r`
|
||||
- Create various types of reminders (one-time, daily, weekly, monthly)
|
||||
- Get notification alerts for your reminders
|
||||
|
||||
The reminder tool provides the following features:
|
||||
|
||||
- **One-time Reminders**: Set for a specific date and time
|
||||
- **Daily Reminders**: Occur every day at the specified time
|
||||
- **Weekly Reminders**: Occur on specific days of the week
|
||||
- **Monthly Reminders**: Occur on a specific day each month or the last day of each month
|
||||
- **Custom Reminders**: Create complex reminder patterns
|
||||
|
||||
*GNOME/MATE comparison:* Similar to calendar applications in GNOME/MATE but with a simplified interface optimized for keyboard navigation.
|
||||
|
||||
|
||||
## Volume and Media Controls
|
||||
|
||||
### System Volume
|
||||
|
||||
- Increase volume: `MODKEY` + `XF86AudioRaiseVolume`
|
||||
- Decrease volume: `MODKEY` + `XF86AudioLowerVolume`
|
||||
- Mute/unmute: `MODKEY` + `XF86AudioMute`
|
||||
|
||||
### Media Player Controls
|
||||
|
||||
- Play/Pause: `XF86AudioPlay`
|
||||
- Next track: `XF86AudioNext`
|
||||
- Previous track: `XF86AudioPrev`
|
||||
- Stop: `XF86AudioStop`
|
||||
- Media info: `MODKEY` + `XF86AudioPlay`
|
||||
|
||||
In Ratpoison mode, these are also available with Alt+Shift combinations:
|
||||
|
||||
- Increase volume: `Alt` + `Shift` + `=`
|
||||
- Decrease volume: `Alt` + `Shift` + `-`
|
||||
- Previous track: `Alt` + `Shift` + `z`
|
||||
- Pause: `Alt` + `Shift` + `c`
|
||||
- Play: `Alt` + `Shift` + `x`
|
||||
- Stop: `Alt` + `Shift` + `v`
|
||||
- Next track: `Alt` + `Shift` + `b`
|
||||
- Media info: `Alt` + `Shift` + `u`
|
||||
|
||||
*GNOME/MATE comparison:* Media controls are similar to those in GNOME/MATE, with the addition of audio feedback.
|
||||
|
||||
|
||||
## File Management
|
||||
|
||||
I38 uses FILEBROWSER for file management. Launch it in Ratpoison mode with the `f` key.
|
||||
|
||||
*GNOME/MATE comparison:* Similar functionality to Nautilus (GNOME) or Caja (MATE), but launched via keyboard shortcut rather than from a desktop icon or menu.
|
||||
|
||||
|
||||
## System Operations
|
||||
|
||||
- Reload I38 configuration: In Ratpoison mode, `Control` + `;` (semicolon)
|
||||
- Exit i3 (log out): In Ratpoison mode, `Control` + `q`
|
||||
- Check battery status: In Ratpoison mode, `Alt` + `b`
|
||||
- Check game controller status: In Ratpoison mode, `g`
|
||||
- Adjust screen brightness (if xrandr is available): In Ratpoison mode, `Alt` + `s`
|
||||
|
||||
*GNOME/MATE comparison:* These functions are typically available through system menus or indicators in GNOME/MATE.
|
||||
|
||||
|
||||
## Keyboard Layouts
|
||||
|
||||
Switch between layouts: `Super` + `Space`
|
||||
|
||||
This is only available if you chose multiple keyboard layouts during setup.
|
||||
|
||||
*GNOME/MATE comparison:* Similar to keyboard layout switching in GNOME/MATE, but with a different default shortcut.
|
||||
|
||||
|
||||
## Desktop and Window Decorations
|
||||
|
||||
Unlike GNOME or MATE, i3 uses minimal window decorations. Windows don't have title bars with minimize/maximize buttons. Instead, windows fill their available space automatically, and interactions are performed through keyboard shortcuts.
|
||||
|
||||
- Show desktop icons: `MODKEY` + `Control` + `d`
|
||||
|
||||
## Clipboard Management
|
||||
|
||||
I38 includes clipboard management features:
|
||||
|
||||
- Access clipboard history: `MODKEY` + `Control` + `c`
|
||||
|
||||
## Bookmark Management
|
||||
|
||||
- Access bookmarks: `MODKEY` + `Control` + `b`
|
||||
|
||||
*GNOME/MATE comparison:* Bookmarks are typically managed within applications in GNOME/MATE. I38 provides a system-wide bookmark manager.
|
||||
|
||||
## Tips for New Users
|
||||
|
||||
- **Use the window list**: When you're lost, use `RATPOISONKEY` then `'` to show all windows in the current workspace.
|
||||
- **Bookmark important websites**: Use `MODKEY` + `Control` + `b` to access bookmarks.
|
||||
- **Remember the help shortcut**: `MODKEY` + `Shift` + `F1` is your friend when you need guidance.
|
||||
- **Let the sound effects guide you**: Pay attention to the audio cues to understand what's happening.
|
||||
- **Take advantage of OCR**: If an application isn't accessible, try the OCR function.
|
||||
|
||||
### Transitioning from GNOME/MATE
|
||||
|
||||
- Start by learning the basic navigation shortcuts before exploring advanced features
|
||||
- The tabbed layout should feel somewhat familiar if you're used to browser tabs
|
||||
- Alt+Tab works similarly to GNOME/MATE for switching between windows
|
||||
- Focus on keyboard commands rather than looking for visual elements like panels or docks
|
||||
|
||||
## Customization
|
||||
|
||||
You can customize I38 by editing the file `~/.config/i3/customizations`. This file will not be overwritten when you update I38.
|
||||
|
||||
Example customizations:
|
||||
|
||||
```
|
||||
# Change background color
|
||||
exec_always --no-startup-id xsetroot -solid "#2E3440"
|
||||
|
||||
# Add custom keybinding
|
||||
bindsym $mod+F12 exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@ 100%
|
||||
```
|
||||
|
||||
To reconfigure I38 completely, run the `i38.sh` script again.
|
||||
|
||||
*GNOME/MATE comparison:* Much more text-based configuration compared to the graphical settings dialogs in GNOME/MATE.
|
||||
|
||||
## Getting Help
|
||||
|
||||
If you need assistance with I38, you can:
|
||||
|
||||
- Press `MODKEY` + `Shift` + `F1` to view the help documentation
|
||||
- Visit the Stormux website at [stormux.org](https://stormux.org)
|
||||
- Join the Stormux irc at irc.stormux.org #stormux or #a11y
|
||||
- Check the i3 documentation at [i3wm.org/docs/userguide.html](https://i3wm.org/docs/userguide.html)
|
||||
|
||||
---
|
||||
|
||||
*I38 - Making i3 accessible. A Stormux project. License: GPL v3*
|
688
LICENSE
688
LICENSE
@ -1,14 +1,674 @@
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
Version 2, December 2004
|
||||
|
||||
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim or modified
|
||||
copies of this license document, and changing it is allowed as long
|
||||
as the name is changed.
|
||||
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. You just DO WHAT THE FUCK YOU WANT TO.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
|
203
README.md
203
README.md
@ -3,9 +3,10 @@
|
||||
Accessibility setup script for the i3 window manager.
|
||||
|
||||
## i38.sh
|
||||
Released under the terms of the WTFPL License: http://www.wtfpl.net
|
||||
Released under the terms of the GPL License Version 3: https://www.gnu.org/licenses/
|
||||
This is a Stormux project: https://stormux.org
|
||||
|
||||
|
||||
## Why the name?
|
||||
|
||||
An uppercase I looks like a 1, 3 from i3, and 8 because the song [We Are 138](https://www.youtube.com/watch?v=-n2Mkdw4q44) rocks!
|
||||
@ -13,29 +14,213 @@ An uppercase I looks like a 1, 3 from i3, and 8 because the song [We Are 138](ht
|
||||
|
||||
## Requirements
|
||||
|
||||
- i3-wm: The i3 window manager.
|
||||
- acpi: [optional] for battery status. It will still work even without this package, but uses it if it is installed.
|
||||
- dex: [optional] Alternative method for auto starting applications.
|
||||
- acpi: [optional] for battery status. It will still work even without this package, but uses it if it is installed. Required for the battery monitor with sound alerts.
|
||||
- bc: For the information panel.
|
||||
- clipster: clipboard manager
|
||||
- dex: [optional] Alternative method for auto starting applications.
|
||||
- i3-wm: The i3 window manager.
|
||||
- jq: for getting the current workspace
|
||||
- libcanberra: [optional] To play the desktop login sound.
|
||||
- libnotify: For sending notifications
|
||||
- lxsession: [optional] For GUI power options like shutdown
|
||||
- magic-wormhole: [optional] for file sharing with magic-wormhole GUI
|
||||
- notification-daemon: To handle notifications
|
||||
- ocrdesktop: For getting contents of the current window with OCR.
|
||||
- pamixer: for the mute-unmute script
|
||||
- pandoc or markdown: To generate html files.
|
||||
- pcmanfm: [optional] Graphical file manager.
|
||||
- playerctl: music controls
|
||||
- python-gobject: for applications menu.
|
||||
- python-i3ipc: for sounds etc.
|
||||
- sgtk-menu: for applications menu
|
||||
- python-pillow: For OCR
|
||||
- python-pytesseract: For OCR
|
||||
- remind: [optional] For reminder notifications, Requires notify-daemon and notify-send for automatic reminders.
|
||||
scrot: For OCR
|
||||
- sox: for sounds.
|
||||
- transfersh: [optional] for file sharing GUI
|
||||
- tesseract: For OCR
|
||||
- tesseract-data-eng: For OCR
|
||||
- udiskie: [optional] for automatically mounting removable storage
|
||||
- x11bell: [optional] Bell support if you do not have a PC speaker. Available from https://github.com/jovanlanik/x11bell
|
||||
- xbacklight: [optional] for screen brightness adjustment
|
||||
- xclip: Clipboard support
|
||||
- xfce4-notifyd: For sending notifications. Replaces notification-daemon (Sway users will need to install the customized variant at <https://github.com/icasdri/xfce4-notifyd-layer-shell>)
|
||||
- xorg-setxkbmap: [optional] for multiple keyboard layouts
|
||||
- yad: For screen reader accessible dialogs
|
||||
|
||||
I38 will try to detect your browser, file manager, and web browser and present you with a list of options to bind to their launch keys. It will also create bindings for pidgin and mumble if they are installed. To use the bindings, press your ratpoison mode key which is set when you run the i38.sh script. next, press the binding for the application you want, w for web browser, e for text editor, f for file manager, m for mumble, etc. To learn all the bindings, find and read the mode ratpoison section of ~/.config/i3/config.
|
||||
I38 will try to detect your browser, file manager, and text editor and present you with a list of options to bind to their launch keys. It will also create bindings for pidgin and mumble if they are installed. Additionally, you can now add custom applications with your own keybindings during the setup process. To use the bindings, press your ratpoison mode key which is set when you run the i38.sh script. Next, press the binding for the application you want; w for web browser, e for text editor, f for file manager, m for mumble, etc. To learn all the bindings, find and read the mode ratpoison section of ~/.config/i3/config or use the help binding key, alt or super depending on your settings with Shift+F1.
|
||||
|
||||
Ratpoison mode is now enabled by default for better accessibility and ease of use.
|
||||
|
||||
Sound themes can be configured using GTK configuration files or gsettings. Replace "name" with the name of the theme you want to use.
|
||||
|
||||
Note that if you enable all sound events as shown below, you'll also hear GTK sounds when moving around menus, buttons, etc, if the sound theme has sounds for those events.
|
||||
|
||||
To configure the theme name with gsettings, do as follows:
|
||||
|
||||
gsettings set org.gnome.desktop.sound theme-name name
|
||||
|
||||
If you'd like all sound types enabled:
|
||||
|
||||
gsettings set org.gnome.desktop.sound input-feedback-sounds true
|
||||
|
||||
gsettings set org.gnome.desktop.sound event-sounds true
|
||||
|
||||
To configure with a config file, edit or create ~/.config/gtk-3.0/settings.ini
|
||||
|
||||
[Settings]
|
||||
gtk-enable-event-sounds=1
|
||||
gtk-enable-input-feedback-sounds=1
|
||||
gtk-sound-theme-name=name
|
||||
gtk-modules=canberra-gtk-module
|
||||
|
||||
You can apply the same configuration to GTK2 apps. Create or edit ~/.gtkrc-2.0
|
||||
|
||||
gtk-enable-event-sounds=1
|
||||
gtk-enable-input-feedback-sounds=1
|
||||
gtk-sound-theme-name=name
|
||||
gtk-modules=canberra-gtk-module
|
||||
|
||||
## Usage:
|
||||
|
||||
- With no arguments, create the i3 configuration.
|
||||
- -h: This help screen.
|
||||
- -h: Help screen.
|
||||
- -u: Copy over the latest version of scripts.
|
||||
- -x: Generate ~/.xinitrc and ~/.xprofile.
|
||||
- -X: Generate ~/.xprofile only.
|
||||
|
||||
|
||||
## Ratpoison Mode
|
||||
|
||||
I38 is an adaptation of the old Strychnine project which was based on the Ratpoison window manager. Ratpoison is a screen-like window manager, and the important concept from that, which applies to I38, is adding keyboard shortcuts without conflicting application shortcuts. This is done with an "escape key".
|
||||
|
||||
When creating I38, I really wanted to port that functionality over, because it is very powerful and allows for lots and lots of shortcuts while minimizing collisions between shortcuts. So, for example, if you have chosen brave as your web browser, and selected alt+escape as your ratpoison mode key, you can quickly launch brave by pressing alt+escape followed by the letter w.
|
||||
|
||||
|
||||
## Custom Applications in Ratpoison Mode
|
||||
|
||||
I38 now includes a powerful system for adding custom applications to ratpoison mode with user-defined keybindings. During the configuration process, after setting up window event sounds, you'll be prompted to add custom applications that will be accessible through ratpoison mode.
|
||||
|
||||
### How It Works
|
||||
|
||||
The system will prompt you through a series of questions for each application you want to add:
|
||||
|
||||
1. **Application Name**: A descriptive name for the application
|
||||
2. **Execution Command**: The full path or command to run the application
|
||||
3. **Command Flags**: Optional command-line arguments
|
||||
4. **Keybinding**: A custom key combination using I38's notation system
|
||||
|
||||
### Keybinding Notation System
|
||||
|
||||
I38 uses a simple notation system to define keybindings that supports modifiers and special keys:
|
||||
|
||||
#### Modifiers
|
||||
- `^` = Control (Ctrl)
|
||||
- `!` = Alt
|
||||
- `#` = Super (Windows/Cmd key)
|
||||
- `m` = Your chosen mod key (configured during setup)
|
||||
- Uppercase letters automatically add Shift (e.g., `C` = Shift+c)
|
||||
|
||||
#### Special Keys
|
||||
- **Function Keys**: `f1`, `f2`, ... `f12`
|
||||
- **Arrow Keys**: `up`, `down`, `left`, `right`
|
||||
- **Navigation Keys**: `home`, `end`, `pageup`, `pagedown`
|
||||
- **Editing Keys**: `insert`, `delete`, `backspace`
|
||||
- **Other Keys**: `space`, `tab`, `return`, `escape`, `print`
|
||||
|
||||
#### Example Keybindings
|
||||
- `c` = Just the 'c' key
|
||||
- `^c` = Control+c
|
||||
- `!f1` = Alt+F1
|
||||
- `mspace` = Your mod key + Space
|
||||
- `^!up` = Control+Alt+Up arrow
|
||||
- `#pagedown` = Super+Page Down
|
||||
- `C` = Shift+c
|
||||
- `^C` = Control+Shift+c
|
||||
|
||||
### Example Configuration Session
|
||||
|
||||
Here's what the configuration prompts look like:
|
||||
|
||||
```
|
||||
Enter application name (or press enter when finished): Discord
|
||||
Enter execution path/command for Discord: discord
|
||||
Enter command line flags for Discord (optional): --no-sandbox
|
||||
Enter keybinding for Discord (Examples: c, ^c, !f1, mspace, ^!up) or ? for help: d
|
||||
```
|
||||
|
||||
Another example:
|
||||
```
|
||||
Enter application name (or press enter when finished): Terminal Calculator
|
||||
Enter execution path/command for Terminal Calculator: gnome-calculator
|
||||
Enter command line flags for Terminal Calculator (optional):
|
||||
Enter keybinding for Terminal Calculator (Examples: c, ^c, !f1, mspace, ^!up) or ? for help: ^#c
|
||||
```
|
||||
|
||||
### Getting Help During Configuration
|
||||
|
||||
If you type `?` when prompted for a keybinding, I38 will display a comprehensive help screen with:
|
||||
- Complete modifier notation reference
|
||||
- List of all supported special keys
|
||||
- Multiple examples of different keybinding combinations
|
||||
- Rules about uppercase letters and Shift
|
||||
|
||||
### Conflict Detection
|
||||
|
||||
I38 automatically prevents keybinding conflicts by:
|
||||
- Maintaining a list of all existing ratpoison mode bindings
|
||||
- Checking your custom keybinding against reserved keys
|
||||
- Showing an error message if you try to use a taken keybinding
|
||||
- Asking you to choose a different key combination
|
||||
|
||||
### Reserved Keybindings
|
||||
|
||||
The following keys are already used in ratpoison mode and cannot be assigned to custom applications:
|
||||
|
||||
- `c` = Terminal
|
||||
- `e` = Text editor
|
||||
- `f` = File manager
|
||||
- `w` = Web browser
|
||||
- `k` = Kill window
|
||||
- `m` = Mumble (if installed)
|
||||
- `p` = Pidgin (if installed)
|
||||
- `g` = Game controller status
|
||||
- Various modifier combinations for volume, music controls, etc.
|
||||
|
||||
### Using Your Custom Applications
|
||||
|
||||
Once configured, access your custom applications by:
|
||||
1. Press your ratpoison mode key (e.g., Alt+Escape)
|
||||
2. Press your custom keybinding (e.g., `d` for Discord)
|
||||
3. The application launches and ratpoison mode exits
|
||||
|
||||
### Advanced Examples
|
||||
|
||||
**Media Applications:**
|
||||
- `vlc` bound to `v` for VLC media player
|
||||
- `spotify` bound to `^s` for Spotify with Control+s
|
||||
|
||||
**Development Tools:**
|
||||
- `code` bound to `mf2` for VS Code with mod+F2
|
||||
- `gimp` bound to `!g` for GIMP with Alt+g
|
||||
|
||||
**System Tools:**
|
||||
- `htop` bound to `^#h` for system monitor with Control+Super+h
|
||||
- `pavucontrol` bound to `!space` for audio control with Alt+Space
|
||||
|
||||
### Tips for Choosing Keybindings
|
||||
|
||||
1. **Keep it memorable**: Use the first letter of the application name when possible
|
||||
2. **Group related apps**: Use similar modifiers for related applications
|
||||
3. **Consider frequency**: Use simpler keys for frequently used applications
|
||||
4. **Avoid conflicts**: The system will warn you, but plan ahead for a logical layout
|
||||
|
||||
|
||||
## Panel Mode
|
||||
|
||||
Very similar to ratpoison, accessed with Alt+Control+Tab. It contains items that would normally be found in a traditional desktop's panel. For example, it has a simple note app, system information, weather, bluetooth and power options, and remind.
|
||||
|
||||
|
||||
## I38 Help
|
||||
|
||||
To get help for I38, you can press the top level keybinding alt+shift+F1. It is also available by pressing the ratpoison mode key followed by question mark. A limitation of yad, which is used to display the help text means that the cursor starts at the bottom of the text. Please press control+home to jump to the top. You can navigate with the arrow keys, and use control+f to find text within the document.
|
||||
|
||||
The help text is a modified version of the configuration file itself that is intended to be easier to read. I have tried to add in comments that should also serve to make things more clear.
|
||||
|
644
i38.sh
644
i38.sh
@ -1,16 +1,26 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Configures the i3 window manager to make it screen reader accessible
|
||||
# Written by Storm Dragon, Jeremiah, and contributers.
|
||||
# Released under the terms of the WTFPL http://www.wtfpl.net
|
||||
# Copyright Stormux, 2022
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||
# PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
# Flag for sway configurations
|
||||
usingSway=1 # Not by default.
|
||||
i3Path="${XDG_CONFIG_HOME:-$HOME/.config}/i3"
|
||||
i3msg="i3-msg"
|
||||
|
||||
# Dialog accessibility
|
||||
export DIALOGOPTS='--no-lines --visit-items'
|
||||
|
||||
# Check to make sure minimum requirements are installed.
|
||||
for i in dialog jq sgtk-menu yad ; do
|
||||
for i in dialog jq yad ; do
|
||||
if ! command -v "$i" &> /dev/null ; then
|
||||
missing+=("$i")
|
||||
fi
|
||||
@ -23,6 +33,115 @@ if [[ -n "${missing}" ]]; then
|
||||
echo "${missing[*]}"
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v pandoc &> /dev/null && ! command -v markdown &> /dev/null ; then
|
||||
echo "Please install either pandoc or markdown."
|
||||
echo "The markdown command may be provided by the package discount."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
keyboard_menu() {
|
||||
keyboardMenu=("us" "English (US)"
|
||||
"af" "Dari"
|
||||
"al" "Albanian"
|
||||
"et" "Amharic"
|
||||
"ara" "Arabic"
|
||||
"ma" "Arabic (Morocco)"
|
||||
"sy" "Arabic (Syria)"
|
||||
"am" "Armenian"
|
||||
"az" "Azerbaijani"
|
||||
"ml" "Bambara"
|
||||
"bd" "Bangla"
|
||||
"by" "Belarusian"
|
||||
"be" "Belgian"
|
||||
"dz" "Berber (Algeria, Latin)"
|
||||
"ba" "Bosnian"
|
||||
"brai" "Braille"
|
||||
"bg" "Bulgarian"
|
||||
"mm" "Burmese"
|
||||
"cn" "Chinese"
|
||||
"hr" "Croatian"
|
||||
"cz" "Czech"
|
||||
"dk" "Danish"
|
||||
"mv" "Dhivehi"
|
||||
"nl" "Dutch"
|
||||
"bt" "Dzongkha"
|
||||
"au" "English (Australia)"
|
||||
"gh" "English (Ghana)"
|
||||
"ng" "English (Nigeria)"
|
||||
"za" "English (South Africa)"
|
||||
"gb" "English (UK)"
|
||||
"epo" "Esperanto"
|
||||
"ee" "Estonian"
|
||||
"fo" "Faroese"
|
||||
"ph" "Filipino"
|
||||
"fi" "Finnish"
|
||||
"fr" "French"
|
||||
"ca" "French (Canada)"
|
||||
"cd" "French (Democratic Republic of the Congo)"
|
||||
"ge" "Georgian"
|
||||
"de" "German"
|
||||
"ch" "German (Switzerland)"
|
||||
"gr" "Greek"
|
||||
"il" "Hebrew"
|
||||
"hu" "Hungarian"
|
||||
"is" "Icelandic"
|
||||
"in" "Indian"
|
||||
"jv" "Indonesian (Javanese)"
|
||||
"iq" "Iraqi"
|
||||
"ie" "Irish"
|
||||
"it" "Italian"
|
||||
"jp" "Japanese"
|
||||
"nec_vndr/jp" "Japanese (PC-98)"
|
||||
"kz" "Kazakh"
|
||||
"kh" "Khmer (Cambodia)"
|
||||
"kr" "Korean"
|
||||
"kg" "Kyrgyz"
|
||||
"la" "Lao"
|
||||
"lv" "Latvian"
|
||||
"lt" "Lithuanian"
|
||||
"mk" "Macedonian"
|
||||
"mv" "Malay (Jawi, Arabic Keyboard)"
|
||||
"mt" "Maltese"
|
||||
"mao" "Maori"
|
||||
"mn" "Mongolian"
|
||||
"me" "Montenegrin"
|
||||
"gn" "N'Ko (AZERTY)"
|
||||
"np" "Nepali"
|
||||
"no" "Norwegian"
|
||||
"ir" "Persian"
|
||||
"pl" "Polish"
|
||||
"pt" "Portuguese"
|
||||
"br" "Portuguese (Brazil)"
|
||||
"ro" "Romanian"
|
||||
"ru" "Russian"
|
||||
"rs" "Serbian"
|
||||
"lk" "Sinhala (phonetic)"
|
||||
"sk" "Slovak"
|
||||
"si" "Slovenian"
|
||||
"es" "Spanish"
|
||||
"latam" "Spanish (Latin American)"
|
||||
"tz" "Swahili (Tanzania)"
|
||||
"se" "Swedish"
|
||||
"tw" "Taiwanese"
|
||||
"tj" "Tajik"
|
||||
"th" "Thai"
|
||||
"tr" "Turkish"
|
||||
"tm" "Turkmen"
|
||||
"bw" "Tswana"
|
||||
"ua" "Ukrainian"
|
||||
"pk" "Urdu (Pakistan)"
|
||||
"uz" "Uzbek (Afghanistan)"
|
||||
"vn" "Vietnamese"
|
||||
"sn" "Wolof"
|
||||
)
|
||||
dialog --title "I38" \
|
||||
--backtitle "Use the arrow keys to find the option you want, and enter to select it. When you are finished selecting layouts, use right arrow to find \"Done\" and press enter." \
|
||||
--clear \
|
||||
--cancel-label "Done" \
|
||||
--no-tags \
|
||||
--menu "Select Keyboard Layout" 0 0 0 "${keyboardMenu[@]}" --stdout
|
||||
return $?
|
||||
}
|
||||
|
||||
menulist() {
|
||||
# Args: List of items for menu.
|
||||
@ -37,7 +156,7 @@ menulist() {
|
||||
--backtitle "Use the arrow keys to find the option you want, and enter to select it." \
|
||||
--clear \
|
||||
--no-tags \
|
||||
--menu "$menuText" 0 0 0 ${menuList[@]} --stdout
|
||||
--menu "$menuText" 0 0 0 "${menuList[@]}" --stdout
|
||||
return $?
|
||||
}
|
||||
|
||||
@ -57,11 +176,167 @@ yesno() {
|
||||
# Args: Question to user.
|
||||
dialog --clear --title "I38" --yesno "$*" -1 -1 --stdout
|
||||
echo $?
|
||||
}
|
||||
|
||||
# Custom application keybinding functions
|
||||
declare -A usedKeys
|
||||
declare -a customApps
|
||||
|
||||
showKeybindingHelp() {
|
||||
dialog --title "I38 Keybinding Help" --msgbox \
|
||||
"Keybinding Notation:
|
||||
Modifiers: ^ = Ctrl, ! = Alt, # = Super, m = your mod key
|
||||
Special keys: f1-f12, up/down/left/right, space, tab, return
|
||||
home/end/insert/delete/pageup/pagedown, print
|
||||
backspace, escape
|
||||
|
||||
Examples:
|
||||
c = just 'c' key
|
||||
^c = Ctrl+c
|
||||
!f1 = Alt+F1
|
||||
mspace = mod+Space
|
||||
^!up = Ctrl+Alt+Up
|
||||
#pagedown = Super+Page_Down
|
||||
|
||||
Uppercase letters imply Shift (e.g., C = Shift+c)" 0 0
|
||||
}
|
||||
|
||||
convertKeybinding() {
|
||||
local input="$1"
|
||||
local result=""
|
||||
local baseKey=""
|
||||
|
||||
# Handle modifiers
|
||||
if [[ "$input" == *"^"* ]]; then
|
||||
result+="Control+"
|
||||
input="${input//^/}"
|
||||
fi
|
||||
if [[ "$input" == *"!"* ]]; then
|
||||
result+="Mod1+"
|
||||
input="${input//!/}"
|
||||
fi
|
||||
if [[ "$input" == *"#"* ]]; then
|
||||
result+="Mod4+"
|
||||
input="${input//#/}"
|
||||
fi
|
||||
if [[ "$input" == *"m"* ]]; then
|
||||
result+="\$mod+"
|
||||
input="${input//m/}"
|
||||
fi
|
||||
|
||||
# Handle shift for uppercase letters
|
||||
if [[ "$input" =~ [A-Z] ]]; then
|
||||
result+="Shift+"
|
||||
input="${input,,}"
|
||||
fi
|
||||
|
||||
# Convert special keys
|
||||
case "$input" in
|
||||
f[1-9]|f1[0-2]) baseKey="F${input#f}" ;;
|
||||
up|down|left|right) baseKey="$input" ;;
|
||||
space) baseKey="space" ;;
|
||||
tab) baseKey="Tab" ;;
|
||||
return) baseKey="Return" ;;
|
||||
escape) baseKey="Escape" ;;
|
||||
backspace) baseKey="BackSpace" ;;
|
||||
print) baseKey="Print" ;;
|
||||
home) baseKey="Home" ;;
|
||||
end) baseKey="End" ;;
|
||||
insert) baseKey="Insert" ;;
|
||||
delete) baseKey="Delete" ;;
|
||||
pageup) baseKey="Page_Up" ;;
|
||||
pagedown) baseKey="Page_Down" ;;
|
||||
*) baseKey="$input" ;;
|
||||
esac
|
||||
|
||||
echo "${result}${baseKey}"
|
||||
}
|
||||
|
||||
populateUsedKeys() {
|
||||
# Populate with existing ratpoison mode bindings
|
||||
usedKeys["Shift+slash"]=1
|
||||
usedKeys["c"]=1
|
||||
usedKeys["e"]=1
|
||||
usedKeys["f"]=1
|
||||
usedKeys["\$mod+e"]=1
|
||||
usedKeys["w"]=1
|
||||
usedKeys["k"]=1
|
||||
usedKeys["m"]=1
|
||||
usedKeys["Print"]=1
|
||||
usedKeys["\$mod+r"]=1
|
||||
usedKeys["p"]=1
|
||||
usedKeys["\$mod+s"]=1
|
||||
usedKeys["Mod1+Shift+0"]=1
|
||||
usedKeys["Mod1+Shift+9"]=1
|
||||
usedKeys["Mod1+Shift+equal"]=1
|
||||
usedKeys["Mod1+Shift+minus"]=1
|
||||
usedKeys["Mod1+Shift+z"]=1
|
||||
usedKeys["Mod1+Shift+c"]=1
|
||||
usedKeys["Mod1+Shift+x"]=1
|
||||
usedKeys["Mod1+Shift+v"]=1
|
||||
usedKeys["Mod1+Shift+b"]=1
|
||||
usedKeys["Mod1+Shift+u"]=1
|
||||
usedKeys["Mod1+b"]=1
|
||||
usedKeys["g"]=1
|
||||
usedKeys["apostrophe"]=1
|
||||
usedKeys["Shift+c"]=1
|
||||
usedKeys["Shift+o"]=1
|
||||
usedKeys["Shift+t"]=1
|
||||
usedKeys["Control+semicolon"]=1
|
||||
usedKeys["Control+Shift+semicolon"]=1
|
||||
usedKeys["Shift+exclam"]=1
|
||||
usedKeys["Control+q"]=1
|
||||
usedKeys["Escape"]=1
|
||||
usedKeys["Control+g"]=1
|
||||
}
|
||||
|
||||
inputText() {
|
||||
# Args: prompt text
|
||||
dialog --title "I38" --inputbox "$1" 0 0 --stdout
|
||||
}
|
||||
|
||||
addCustomApplication() {
|
||||
local appName appCommand appFlags keybinding convertedKey
|
||||
|
||||
populateUsedKeys
|
||||
|
||||
while true; do
|
||||
appName="$(inputText "Custom Applications:\n\nEnter application name (or press enter when finished):")"
|
||||
[[ -z "$appName" ]] && break
|
||||
|
||||
appCommand="$(inputText "Enter execution path/command for $appName:")"
|
||||
[[ -z "$appCommand" ]] && continue
|
||||
|
||||
appFlags="$(inputText "Enter command line flags for $appName (optional):")"
|
||||
|
||||
while true; do
|
||||
keybinding="$(inputText "Enter keybinding for $appName (Examples: c, ^c, !f1, mspace, ^!up) or ? for help:")"
|
||||
|
||||
if [[ "$keybinding" == "?" ]]; then
|
||||
showKeybindingHelp
|
||||
continue
|
||||
fi
|
||||
|
||||
[[ -z "$keybinding" ]] && break
|
||||
|
||||
convertedKey="$(convertKeybinding "$keybinding")"
|
||||
|
||||
if [[ -n "${usedKeys[$convertedKey]}" ]]; then
|
||||
dialog --title "I38" --msgbox "Keybinding '$keybinding' ($convertedKey) is already in use. Please choose another." 0 0
|
||||
continue
|
||||
fi
|
||||
|
||||
# Add to arrays
|
||||
customApps+=("$appName|$appCommand|$appFlags|$convertedKey")
|
||||
usedKeys["$convertedKey"]=1
|
||||
break
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
help() {
|
||||
echo "${0##*/}"
|
||||
echo "Released under the terms of the WTFPL License: http://www.wtfpl.net"
|
||||
echo "Released under the terms of the GPL V3 License: https://www.gnu.org/licenses/"
|
||||
echo -e "This is a Stormux project: https://stormux.org\n"
|
||||
echo -e "Usage:\n"
|
||||
echo "With no arguments, create the i3 configuration."
|
||||
@ -114,6 +389,7 @@ fi
|
||||
cat << 'EOF' > ~/.xprofile
|
||||
# Accessibility variables
|
||||
export ACCESSIBILITY_ENABLED=1
|
||||
export CHROME_FLAGS="--force-renderer-accessibility"
|
||||
export GTK_MODULES=gail:atk-bridge
|
||||
export GNOME_ACCESSIBILITY=1
|
||||
export QT_ACCESSIBILITY=1
|
||||
@ -145,6 +421,8 @@ while getopts "${args}" i ; do
|
||||
case "$i" in
|
||||
h) help;;
|
||||
s)
|
||||
swaySystemIncludesPath="/etc/sway/config.d"
|
||||
usingSway=0
|
||||
i3msg="swaymsg"
|
||||
i3Path="${XDG_CONFIG_HOME:-$HOME/.config}/sway"
|
||||
;;
|
||||
@ -154,15 +432,18 @@ while getopts "${args}" i ; do
|
||||
esac
|
||||
done
|
||||
|
||||
# Mod1 alt
|
||||
# Mod4 super
|
||||
# Mod2 and Mod3 not usually defined.
|
||||
|
||||
# Configuration questions
|
||||
export i3Mode=$(yesno "Would you like to use ratpoison mode? This behaves more like strychnine, with an escape key followed by keybindings.")
|
||||
# Ratpoison mode is enabled by default
|
||||
export i3Mode=0
|
||||
# Prevent setting ratpoison mode key to the same as default mode key
|
||||
while [[ "$escapeKey" == "$mod" ]]; do
|
||||
if [[ $i3Mode -eq 0 ]]; then
|
||||
escapeKey="$(menulist "Ratpoison mode key:" Control+t Control+z Control+Escape Alt+Escape Control+Space Super)"
|
||||
escapeKey="${escapeKey//Alt/Mod1}"
|
||||
escapeKey="${escapeKey//Super/Mod4}"
|
||||
fi
|
||||
escapeKey="$(menulist "Ratpoison mode key:" Control+t Control+z Control+Escape Alt+Escape Control+Space Super)"
|
||||
escapeKey="${escapeKey//Alt/Mod1}"
|
||||
escapeKey="${escapeKey//Super/Mod4}"
|
||||
mod="$(menulist "I3 mod key, for top level bindings:" Alt Control Super)"
|
||||
mod="${mod//Alt/Mod1}"
|
||||
mod="${mod//Super/Mod4}"
|
||||
@ -170,11 +451,52 @@ while [[ "$escapeKey" == "$mod" ]]; do
|
||||
dialog --title "I38" --msgbox "Ratpoison and mod key cannot be the same key." -1 -1
|
||||
fi
|
||||
done
|
||||
# Multiple keyboard layouts
|
||||
if [[ $(yesno "Do you want to use multiple keyboard layouts?") -eq 0 ]]; then
|
||||
unset kbd
|
||||
while : ; do
|
||||
kbd+=("$(keyboard_menu)") || break
|
||||
done
|
||||
fi
|
||||
# Volume jump
|
||||
volumeJump=$(rangebox "How much should pressing the volume keys change the volume?" 1 15 5)
|
||||
# Screen Reader
|
||||
unset programList
|
||||
for i in cthulhu orca ; do
|
||||
if command -v ${i/#-/} &> /dev/null ; then
|
||||
if [ -n "$programList" ]; then
|
||||
programList="$programList $i"
|
||||
else
|
||||
programList="$i"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
if [ "$programList" != "${programList// /}" ]; then
|
||||
screenReader="$(menulist ":Screen Reader" $programList)"
|
||||
else
|
||||
screenReader="${programList/#-/}"
|
||||
fi
|
||||
export screenReader="$(command -v $screenReader)"
|
||||
# Email client
|
||||
unset programList
|
||||
for i in betterbird evolution thunderbird ; do
|
||||
if command -v ${i/#-/} &> /dev/null ; then
|
||||
if [ -n "$programList" ]; then
|
||||
programList="$programList $i"
|
||||
else
|
||||
programList="$i"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
if [ "$programList" != "${programList// /}" ]; then
|
||||
emailClient="$(menulist "Email client:" $programList)"
|
||||
else
|
||||
emailClient="${programList/#-/}"
|
||||
fi
|
||||
export emailClient="$(command -v $emailClient)"
|
||||
# Web browser
|
||||
unset programList
|
||||
for i in brave chromium epiphany firefox google-chrome-stable midori seamonkey ; do
|
||||
for i in brave chromium epiphany firefox google-chrome-stable google-chrome-unstable microsoft-edge-stable microsoft-edge-beta microsoft-edge-dev midori seamonkey vivaldi ; do
|
||||
if command -v ${i/#-/} &> /dev/null ; then
|
||||
if [ -n "$programList" ]; then
|
||||
programList="$programList $i"
|
||||
@ -188,7 +510,7 @@ if [ "$programList" != "${programList// /}" ]; then
|
||||
else
|
||||
webBrowser="${programList/#-/}"
|
||||
fi
|
||||
webBrowser="$(command -v $webBrowser)"
|
||||
export webBrowser="$(command -v $webBrowser)"
|
||||
# Text editor
|
||||
unset programList
|
||||
for i in emacs geany gedit kate kwrite l3afpad leafpad libreoffice mousepad pluma ; do
|
||||
@ -205,7 +527,7 @@ textEditor="$(menulist "Text editor:" $programList)"
|
||||
else
|
||||
textEditor="${programList/#-/}"
|
||||
fi
|
||||
textEditor="$(command -v $textEditor)"
|
||||
export textEditor="$(command -v $textEditor)"
|
||||
# File browser
|
||||
# Configure file browser
|
||||
unset programList
|
||||
@ -223,19 +545,30 @@ if [ "$programList" != "${programList// /}" ]; then
|
||||
else
|
||||
fileBrowser="${programList/#-/}"
|
||||
fi
|
||||
fileBrowser="$(command -v $fileBrowser)"
|
||||
export fileBrowser="$(command -v $fileBrowser)"
|
||||
# Auto mount removable media
|
||||
udiskie=1
|
||||
if command -v udiskie &> /dev/null ; then
|
||||
export udiskie=$(yesno "Would you like removable drives to automatically mount when plugged in?")
|
||||
fi
|
||||
# Auto start with dex
|
||||
dex=1
|
||||
if command -v dex &> /dev/null ; then
|
||||
export dex=$(yesno "Would you like to autostart applications with dex?")
|
||||
fi
|
||||
if [[ $dex -eq 0 ]]; then
|
||||
dex -t "${XDG_CONFIG_HOME:-${HOME}/.config}/autostart" -c $(command -v orca)
|
||||
dex -t "${XDG_CONFIG_HOME:-${HOME}/.config}/autostart" -c $(command -v $screenReader)
|
||||
fi
|
||||
if command -v acpi &> /dev/null ; then
|
||||
batteryAlert=1
|
||||
batteryAlert=$(yesno "Do you want low battery notifications?")
|
||||
fi
|
||||
brlapi=1
|
||||
brlapi=$(yesno "Do you want to use a braille display with Orca?")
|
||||
brlapi=$(yesno "Do you want to use a braille display with ${screenReader##*/}?")
|
||||
sounds=1
|
||||
sounds=$(yesno "Do you want window event sounds?")
|
||||
# Custom applications for ratpoison mode
|
||||
addCustomApplication
|
||||
|
||||
if [[ -d "${i3Path}" ]]; then
|
||||
yesno "This will replace your existing configuration at ${i3Path}. Do you want to continue?" || exit 0
|
||||
@ -248,10 +581,20 @@ mkdir -p "${i3Path}"
|
||||
cp -rv scripts/ "${i3Path}/" | dialog --backtitle "I38" --progressbox "Moving scripts into place and writing config..." -1 -1
|
||||
|
||||
cat << EOF > ${i3Path}/config
|
||||
# Generated by I38 (${0##*/}) https://github.com/stormdragon2976/I38
|
||||
# Generated by I38 (${0##*/}) https://git.stormux.org/storm/I38
|
||||
# $(date '+%A, %B %d, %Y at %I:%M%p')
|
||||
EOF
|
||||
|
||||
# If we are using Sway, we need to load in the system configuration
|
||||
# Usually, this is for system specific dBus things that the distro knows how to manage; we should trust their judgment with that
|
||||
if [[ $usingSway ]] && [[ -d "${swaySystemIncludesPath}" ]]; then
|
||||
cat << EOF >> ${i3Path}/config
|
||||
# Include your distribution Sway configuration files.
|
||||
include ${swaySystemIncludesPath}/*
|
||||
EOF
|
||||
fi
|
||||
|
||||
cat << EOF >> ${i3Path}/config
|
||||
# i3 config file (v4)
|
||||
#
|
||||
# Please see https://i3wm.org/docs/userguide.html for a complete reference!
|
||||
@ -279,6 +622,9 @@ bindsym \$mod+Shift+F1 exec ${i3Path}/scripts/i38-help.sh
|
||||
# Run dialog
|
||||
bindsym \$mod+F2 exec ${i3Path}/scripts/run_dialog.sh
|
||||
|
||||
# Bookmarks dialog
|
||||
bindsym \$mod+Control+b exec ${i3Path}/scripts/bookmarks.sh
|
||||
|
||||
# Clipboard manager
|
||||
bindsym \$mod+Control+c exec clipster -s
|
||||
|
||||
@ -288,7 +634,7 @@ bindsym \$mod+Control+Delete exec --no-startup-id sgtk-bar
|
||||
# Use pactl to adjust volume in PulseAudio.
|
||||
bindsym \$mod+XF86AudioRaiseVolume exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@ +${volumeJump}% & play -qnG synth 0.03 sin 440
|
||||
bindsym \$mod+XF86AudioLowerVolume exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@ -${volumeJump}% & play -qnG synth 0.03 sin 440
|
||||
bindsym \$mod+XF86AudioMute exec --no-startup-id ${i3Path}/scripts/mute-unmute.sh
|
||||
bindsym \$mod+XF86AudioMute exec --no-startup-id ${i3Path}/scrip/ts/mute-unmute.sh
|
||||
|
||||
# Music player controls
|
||||
# Requires playerctl.
|
||||
@ -302,16 +648,16 @@ bindsym XF86AudioStop exec --no-startup-id play -qV0 "| sox -np synth 0.03 sin 2
|
||||
bindsym XF86AudioNext exec --no-startup-id play -qV0 "| sox -np synth 0.03 sin 2000 pad 0 .02" "| sox -np synth 0.03 sin 2000" norm 1.0 vol 0.4 & ${i3Path}/scripts/music_controler.sh next
|
||||
|
||||
# start a terminal
|
||||
bindsym \$mod+Return exec i3-sensible-terminal
|
||||
bindsym \$mod+Return exec ${i3Path}/scripts/i3-sensible-terminal.sh
|
||||
|
||||
# kill focused window
|
||||
bindsym \$mod+F4 kill
|
||||
|
||||
# Applications menu
|
||||
bindsym \$mod+F1 exec --no-startup-id sgtk-menu -f
|
||||
bindsym \$mod+F1 exec --no-startup-id "${i3Path}/scripts/menu.py"
|
||||
|
||||
# Desktop icons
|
||||
bindsym \$mod+Control+d exec --no-startup-id yad --icons --compact --no-buttons --title="Desktop" --close-on-unfocus --read-dir=${HOME}/Desktop
|
||||
bindsym \$mod+Control+d exec --no-startup-id ${i3Path}/scripts/desktop.sh
|
||||
|
||||
# change focus
|
||||
# alt+tab and alt+shift+tab
|
||||
@ -322,9 +668,13 @@ bindsym Mod1+Tab focus right
|
||||
bindsym \$mod+BackSpace fullscreen toggle
|
||||
|
||||
|
||||
# move the currently focused window to the scratchpad
|
||||
# Move the currently focused window to the scratchpad
|
||||
bindsym \$mod+Shift+minus move scratchpad
|
||||
|
||||
# Bind the currently focused window to the scratchpad
|
||||
# This means it will always open in the scratchpad
|
||||
bindsym \$mod+Shift+equal exec --no-startup-id ${i3Path}/scripts/bind_to_scratchpad.sh
|
||||
|
||||
# Show the next scratchpad window or hide the focused scratchpad window.
|
||||
# If there are multiple scratchpad windows, this command cycles through them.
|
||||
bindsym \$mod+minus scratchpad show
|
||||
@ -367,6 +717,63 @@ bindsym Control+Shift+F9 move container to workspace number \$ws9, exec spd-say
|
||||
bindsym Control+Shift+F10 move container to workspace number \$ws10, exec spd-say -P important -Cw "moved to workspace 10"
|
||||
|
||||
|
||||
# A mode that will pass all keys except $mod+shift+backspace to the current application.
|
||||
# Use $mod+shift+backspace to exit the mode.
|
||||
bindsym $mod+shift+BackSpace mode "bypass"
|
||||
mode "bypass" {
|
||||
# Exit bypass mode.
|
||||
bindsym $mod+Shift+BackSpace mode "default"
|
||||
}
|
||||
|
||||
|
||||
EOF
|
||||
|
||||
# ocr through speech-dispatcher
|
||||
echo "bindsym ${mod}+F5 exec ${i3Path}/scripts/ocr.py" >> ${i3Path}/config
|
||||
# Interrupt speech-dispatcher output
|
||||
echo "bindsym ${mod}+Shift+F5 exec spd-say -C" >> ${i3Path}/config
|
||||
|
||||
# Multiple keyboard layouts if requested.
|
||||
if [[ ${#kbd[@]} -gt 1 ]]; then
|
||||
echo "bindsym Mod4+space exec ${i3Path}/scripts/keyboard.sh cycle ${kbd[@]}" >> ${i3Path}/config
|
||||
fi
|
||||
|
||||
# Create panel mode
|
||||
cat << EOF >> ${i3Path}/config
|
||||
# Panel mode configuration
|
||||
bindsym Control+Mod1+Tab mode "panel"
|
||||
mode "panel" {
|
||||
# Weather information bound to w
|
||||
bindsym w exec --no-startup-id ${i3Path}/scripts/weather.sh, mode "default"
|
||||
|
||||
# Magic wormhole bound to shift+W
|
||||
bindsym Shift+w exec --no-startup-id ${i3Path}/scripts/wormhole.py, mode "default"
|
||||
|
||||
# System information bound to s
|
||||
bindsym s exec --no-startup-id ${i3Path}/scripts/sysinfo.sh, mode "default"
|
||||
|
||||
$(if command -v remind &> /dev/null ; then
|
||||
echo "# Reminders bound to r"
|
||||
echo "bindsym r exec --no-startup-id ${i3Path}/scripts/reminder.sh, mode \"default\""
|
||||
fi)
|
||||
|
||||
# Simple notes system bound to n
|
||||
bindsym n exec --no-startup-id ${i3Path}/scripts/notes.py, mode "default"
|
||||
|
||||
$(if command -v blueman-manager &> /dev/null ; then
|
||||
echo "# Bluetooth bound to b"
|
||||
echo "bindsym b exec --no-startup-id blueman-manager, mode \"default\""
|
||||
fi)
|
||||
|
||||
$(if command -v lxsession-logout &> /dev/null ; then
|
||||
echo "# Power options bound to p"
|
||||
echo "bindsym p exec --no-startup-id lxsession-logout, mode \"default\""
|
||||
fi)
|
||||
|
||||
# Exit panel mode without any action
|
||||
bindsym Escape mode "default"
|
||||
bindsym Control+g mode "default"
|
||||
}
|
||||
EOF
|
||||
|
||||
# Create ratpoison mode if requested.
|
||||
@ -377,11 +784,15 @@ mode "ratpoison" {
|
||||
# I38 help bound to ?
|
||||
bindsym Shift+slash exec ${i3Path}/scripts/i38-help.sh, mode "default"
|
||||
# Terminal emulator bound to c
|
||||
bindsym c exec i3-sensible-terminal, mode "default"
|
||||
bindsym c exec $sensibleTerminal, mode "default"
|
||||
# Text editor bound to e
|
||||
bindsym e exec $textEditor, mode "default"
|
||||
# File browser bound to f
|
||||
bindsym f exec $fileBrowser, mode "default"
|
||||
$(if [[ ${#fileBrowser} -gt 3 ]]; then
|
||||
echo "# File browser bound to f"
|
||||
echo "bindsym f exec $fileBrowser, mode \"default\""
|
||||
fi)
|
||||
# Email client bound to \$mod+e
|
||||
bindsym \$mod+e exec $emailClient, mode "default"
|
||||
# Web browser bound to w
|
||||
bindsym w exec $webBrowser, mode "default"
|
||||
# Kill window bound to k
|
||||
@ -399,9 +810,9 @@ $(if command -v pidgin &> /dev/null ; then
|
||||
echo "# p bound to pidgin"
|
||||
echo "bindsym p exec $(command -v pidgin), mode \"default\""
|
||||
fi)
|
||||
$(if command -v transfersh &> /dev/null ; then
|
||||
echo "# t bound to share file with transfer.sh"
|
||||
echo 'bindsym t exec bash -c '"'"'fileName="$(yad --title "I38 Upload File" --file)" && url="$(transfersh "${fileName}" | tee >(yad --title "I38 - Uploading ${fileName##*/} ..." --progress --pulsate --auto-close))" && echo "${url#*saved at: }" | tee >(yad --title "I38 - Upload URL" --show-cursor --show-uri --button yad-close --sticky --text-info) >(xclip -selection clipboard)'"', mode \"default\""
|
||||
$(if command -v xrandr &> /dev/null ; then
|
||||
echo "# alt+s bound to brightness control"
|
||||
echo "bindsym \$mod+s exec --no-startup-id ${i3Path}/scripts/screen_controller.sh, mode \"default\""
|
||||
fi)
|
||||
|
||||
#Keyboard based volume Controls with pulseaudio
|
||||
@ -420,15 +831,35 @@ bindsym Mod1+Shift+u exec --no-startup-id play -qV0 "| sox -np synth 0.03 sin 20
|
||||
#Check battery status
|
||||
bindsym Mod1+b exec --no-startup-id ${i3Path}/scripts/battery_status.sh, mode "default"
|
||||
#Check controller battery status
|
||||
bindsym g exec ${i3Path}/scripts/game_controler.sh -s, mode "default"
|
||||
bindsym g exec ${i3Path}/scripts/game_controller.sh -s, mode "default"
|
||||
# Get a list of windows in the current workspace
|
||||
bindsym apostrophe exec --no-startup-id ${i3Path}/scripts/window_list.sh, mode "default"
|
||||
# Restart orca
|
||||
# Restart Cthulhu
|
||||
bindsym Shift+c exec $(command -v cthulhu) --replace, mode "default"
|
||||
# Restart Orca
|
||||
bindsym Shift+o exec $(command -v orca) --replace, mode "default"
|
||||
# reload the configuration file
|
||||
bindsym Control+semicolon exec bash -c '$i3msg -t run_command reload && spd-say -P important -Cw "I38 Configuration reloaded."', mode "default"
|
||||
# restart i3 inplace (preserves your layout/session, can be used to upgrade i3)
|
||||
bindsym Control+Shift+semicolon exec bash -c '$i3msg -t run_command restart && spd-say -P important -Cw "I3 restarted."', mode "default"
|
||||
# Toggle screen reader
|
||||
bindsym Shift+t exec ${i3Path}/scripts/toggle_screenreader.sh, mode "default"
|
||||
$(if [[ $usingSway -eq 0 ]]; then
|
||||
echo "# reload the configuration file"
|
||||
echo "bindsym Control+semicolon exec bash -c '$i3msg -t command reload && spd-say -P important -Cw "I38 Configuration reloaded."', mode "default""
|
||||
|
||||
else
|
||||
echo "# reload the configuration file"
|
||||
echo "bindsym Control+semicolon exec bash -c '$i3msg -t run_command reload && spd-say -P important -Cw "I38 Configuration reloaded."', mode "default""
|
||||
echo "# restart i3 inplace (preserves your layout/session, can be used to upgrade i3)"
|
||||
echo "bindsym Control+Shift+semicolon exec $i3msg -t run_command restart && spd-say -P important -Cw "I3 restarted.", mode "default""
|
||||
fi)
|
||||
# Custom applications
|
||||
$(for app in "${customApps[@]}"; do
|
||||
IFS='|' read -r appName appCommand appFlags appKey <<< "$app"
|
||||
echo "# $appName bound to $appKey"
|
||||
if [[ -n "$appFlags" ]]; then
|
||||
echo "bindsym $appKey exec $appCommand $appFlags, mode \"default\""
|
||||
else
|
||||
echo "bindsym $appKey exec $appCommand, mode \"default\""
|
||||
fi
|
||||
done)
|
||||
# Run dialog with exclamation
|
||||
bindsym Shift+exclam exec ${i3Path}/scripts/run_dialog.sh, mode "default"
|
||||
# exit i3 (logs you out of your X session)
|
||||
@ -442,87 +873,102 @@ bindsym Control+g mode "default"
|
||||
EOF
|
||||
fi
|
||||
|
||||
# For those who do not want ratpoison mode.
|
||||
if [[ -z "${escapeKey}" ]]; then
|
||||
cat << EOF >> ${i3Path}/config
|
||||
# Text editor bound to $mod+e
|
||||
bindsym \$mod+e exec $textEditor
|
||||
# File browser bound to $mod+f
|
||||
bindsym \$mod+f exec $fileBrowser
|
||||
# Web browser bound to $mod+w
|
||||
bindsym \$mod+w exec $webBrowser
|
||||
|
||||
$(if command -v mumble &> /dev/null ; then
|
||||
echo "bindsym \$mod+m exec $(command -v mumble)"
|
||||
fi)
|
||||
$(if command -v ocrdesktop &> /dev/null ; then
|
||||
echo "bindsym Print exec $(command -v ocrdesktop)' -b"
|
||||
fi)
|
||||
$(if command -v pidgin &> /dev/null ; then
|
||||
echo "bindsym \$mod+p exec $(command -v pidgin)"
|
||||
fi)
|
||||
$(if command -v transfersh &> /dev/null ; then
|
||||
echo 'bindsym \$mod+t exec bash -c '"'"'fileName="$(yad --title "I38 Upload File" --file)" && url="$(transfersh "${fileName}" | tee >(yad --title "I38 - Uploading ${fileName##*/} ..." --progress --pulsate --auto-close))" && echo "${url#*saved at: }" | tee >(yad --title "I38 - Upload URL" --show-cursor --show-uri --button yad-close --sticky --text-info) >(xclip -selection clipboard)'"'"
|
||||
fi)
|
||||
#Keyboard based volume controls using pulseaudio
|
||||
bindsym \$mod+Shift+0 exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@ +${volumeJump}% & play -qnG synth 0.03 sin 440
|
||||
bindsym \$mod+Shift+9 exec --no-startup-id pactl set-sink-volume @DEFAULT_SINK@ -${volumeJump}% & play -qnG synth 0.03 sin 440
|
||||
# Music player controls
|
||||
# Requires playerctl.
|
||||
bindsym \$mod+Shift+equal exec --no-startup-id ${i3Path}/scripts/music_controler.sh incvol $volumeJump
|
||||
bindsym \$mod+Shift+minus exec --no-startup-id ${i3Path}/scripts/music_controler.sh decvol $volumeJump
|
||||
bindsym \$mod+Shift+z exec --no-startup-id play -qV0 "| sox -np synth 0.03 sin 2000 pad 0 .02" "| sox -np synth 0.03 sin 2000" norm 1.0 vol 0.4 & ${i3Path}/scripts/music_controler.sh prev
|
||||
bindsym \$mod+Shift+c exec --no-startup-id play -qV0 "| sox -np synth 0.03 sin 2000 pad 0 .02" "| sox -np synth 0.03 sin 2000" norm 1.0 vol 0.4 & ${i3Path}/scripts/music_controler.sh pause
|
||||
bindsym \$mod+Shift+x exec --no-startup-id play -qV0 "| sox -np synth 0.03 sin 2000 pad 0 .02" "| sox -np synth 0.03 sin 2000" norm 1.0 vol 0.4 & ${i3Path}/scripts/music_controler.sh play
|
||||
bindsym \$mod+Shift+v exec --no-startup-id play -qV0 "| sox -np synth 0.03 sin 2000 pad 0 .02" "| sox -np synth 0.03 sin 2000" norm 1.0 vol 0.4 & ${i3Path}/scripts/music_controler.sh stop
|
||||
bindsym \$mod+Shift+b exec --no-startup-id play -qV0 "| sox -np synth 0.03 sin 2000 pad 0 .02" "| sox -np synth 0.03 sin 2000" norm 1.0 vol 0.4 & ${i3Path}/scripts/music_controler.sh next
|
||||
bindsym \$mod+Shift+u exec --no-startup-id play -qV0 "| sox -np synth 0.03 sin 2000 pad 0 .02" "| sox -np synth 0.03 sin 2000" norm 1.0 vol 0.4 & ${i3Path}/scripts/music_controler.sh info
|
||||
#Check battery status
|
||||
bindsym \$mod+b exec --no-startup-id ${i3Path}/scripts/battery_status.sh
|
||||
#Check controller battery status
|
||||
bindsym \$mod+g exec ${i3Path}/scripts/game_controler.sh -s
|
||||
# Get a list of windows in the current workspace
|
||||
bindsym \$mod+apostrophe exec --no-startup-id ${i3Path}/scripts/window_list.sh
|
||||
# Restart orca
|
||||
bindsym \$mod+Shift+o exec $(command -v orca) --replace
|
||||
# reload the configuration file
|
||||
bindsym \$mod+Control+semicolon exec bash -c '$i3msg -t run_command reload && spd-say -P important -Cw "I38 Configuration reloaded."'
|
||||
# restart i3 inplace (preserves your layout/session, can be used to upgrade i3)
|
||||
bindsym \$mod+Control+Shift+semicolon exec bash -c '$i3msg -t run_command restart && spd-say -P important -Cw "I3 restarted."'
|
||||
# exit i3 (logs you out of your X session)
|
||||
bindsym \$mod+Control+q exec bash -c 'yad --image "dialog-question" --title "I38" --button=yes:0 --button=no:1 --text "You pressed the exit shortcut. Do you really want to exit i3? This will end your X session." && $i3msg -t run_command exit'
|
||||
|
||||
|
||||
|
||||
EOF
|
||||
fi
|
||||
|
||||
cat << EOF >> ${i3Path}/config
|
||||
# Auto start section
|
||||
$(if [[ $sounds -eq 0 ]]; then
|
||||
echo "exec_always --no-startup-id ${i3Path}/scripts/sound.py"
|
||||
if [[ $usingSway -eq 0 ]]; then
|
||||
echo "exec --no-startup-id ${i3Path}/scripts/sound.py"
|
||||
else
|
||||
echo "exec_always --no-startup-id ${i3Path}/scripts/sound.py"
|
||||
fi
|
||||
fi
|
||||
if [[ $brlapi -eq 0 ]]; then
|
||||
echo 'exec --no-startup-id xbrlapi --quiet'
|
||||
fi
|
||||
if [[ -x "/usr/lib/notification-daemon-1.0/notification-daemon" ]]; then
|
||||
if [[ $udiskie -eq 0 ]]; then
|
||||
echo 'exec --no-startup-id udiskie'
|
||||
fi
|
||||
if [[ -x "/usr/lib/xfce4/notifyd/xfce4-notifyd" ]]; then
|
||||
echo 'exec_always --no-startup-id /usr/lib/xfce4/notifyd/xfce4-notifyd'
|
||||
elif [[ -x "/usr/lib/notification-daemon-1.0/notification-daemon" ]]; then
|
||||
echo 'exec_always --no-startup-id /usr/lib/notification-daemon-1.0/notification-daemon -r'
|
||||
fi
|
||||
# Work around for weird Void Linux stuff.
|
||||
if [[ -x "/usr/libexec/notification-daemon" ]]; then
|
||||
echo 'exec_always --no-startup-id /usr/libexec/notification-daemon -r'
|
||||
fi
|
||||
if command -v remind &> /dev/null && command -v notify-send &> /dev/null ; then
|
||||
echo "exec_always --no-startup-id ${i3Path}/scripts/launch_remind.sh"
|
||||
touch ~/.reminders
|
||||
fi
|
||||
if [[ $batteryAlert -eq 0 ]]; then
|
||||
echo "exec_always --no-startup-id ${i3Path}/scripts/battery_alert.sh"
|
||||
fi
|
||||
if [[ $dex -eq 0 ]]; then
|
||||
echo '# Start XDG autostart .desktop files using dex. See also'
|
||||
echo '# https://wiki.archlinux.org/index.php/XDG_Autostart'
|
||||
echo 'exec --no-startup-id dex --autostart --environment i3'
|
||||
else
|
||||
echo '# Startup applications'
|
||||
echo 'exec clipster -d'
|
||||
echo 'exec orca'
|
||||
if command -v x11bell &> /dev/null ; then
|
||||
echo 'exec --no-startup-id x11bell play -nqV0 synth .1 sq norm -12'
|
||||
fi
|
||||
echo 'exec --no-startup-id clipster -d'
|
||||
echo "exec $screenReader"
|
||||
echo "exec_always --no-startup-id ${i3Path}/scripts/desktop.sh"
|
||||
fi)
|
||||
|
||||
# First run help documentation
|
||||
exec --no-startup-id bash -c 'if [[ -f "${i3Path}/firstrun" ]]; then ${webBrowser} "${i3Path}/I38.html"& rm "${i3Path}/firstrun"; fi'
|
||||
|
||||
# If you want to add personal customizations to i3, add them in ${i3Path}/customizations
|
||||
# It is not overwritten with the config file is recreated.
|
||||
$(if [[ -r "${i3Path}/customizations" ]]; then
|
||||
echo "include \"${i3Path}/customizations\""
|
||||
else
|
||||
echo "# Rerun the ${0##*/} script after creating the customizations file so it is detected."
|
||||
fi)
|
||||
# It is not overwritten when the config file is recreated.
|
||||
include "${i3Path}/customizations"
|
||||
EOF
|
||||
touch "${i3Path}/customizations"
|
||||
touch "${i3Path}/scratchpad"
|
||||
# Check for markdown or pandoc for converting the welcome document
|
||||
if command -v pandoc &> /dev/null ; then
|
||||
pandoc -f markdown -t html "I38.md" -so "${i3Path}/I38.html" --metadata title="Welcome to I38"
|
||||
elif command -v markdown &> /dev/null ; then
|
||||
cat << EOF > "${i3Path}/I38.html"
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Welcome to I38</title>
|
||||
<meta charset="utf-8">
|
||||
</head>
|
||||
<body>
|
||||
EOF
|
||||
|
||||
# Convert markdown to html and append to the file
|
||||
markdown "I38.md" >> "${i3Path}/I38.html"
|
||||
|
||||
# Close the HTML tags using heredoc
|
||||
cat << EOF >> "${i3Path}/I38.html"
|
||||
</body>
|
||||
</html>
|
||||
EOF
|
||||
fi
|
||||
|
||||
# More readable version of variables.
|
||||
escapeKey="${escapeKey//Mod1/Alt}"
|
||||
escapeKey="${escapeKey//Mod4/Super}"
|
||||
mod="${mod//Mod1/Alt}"
|
||||
mod="${mod//Mod4/Super}"
|
||||
webBrowser="${webBrowser##*/}"
|
||||
screenReader="${screenReader##*/}"
|
||||
textEditor="${textEditor##*/}"
|
||||
fileBrowser="${fileBrowser##*/}"
|
||||
|
||||
# Customize the html file to the user's choices.
|
||||
sed -i -e "s|BROWSER|${webBrowser}|g" \
|
||||
-e "s|MODKEY|${mod}|g" \
|
||||
-e "s|SCREENREADER|${screenReader}|g" \
|
||||
-e "s|RATPOISONKEY|${escapeKey}|g" \
|
||||
-e "s|TEXTEDITOR|${textEditor}|g" \
|
||||
-e "s|FILEBROWSER|${fileBrowser}|g" "${i3Path}/I38.html"
|
||||
|
||||
# Create the firstrun file
|
||||
touch "${i3Path}/firstrun"
|
||||
|
2
panel.conf
Normal file
2
panel.conf
Normal file
@ -0,0 +1,2 @@
|
||||
assign [class="Solaar"] workspace number 11
|
||||
assign [class="qjoypad"] workspace number 11
|
@ -1,11 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
workSpace="$(i3-msg -t get_workspaces \
|
||||
| jq '.[] | select(.focused==true).name' \
|
||||
| cut -d"\"" -f2)"
|
||||
# This file is part of I38.
|
||||
|
||||
# I38 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation,
|
||||
# either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
# I38 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
# PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with I38. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
path="$(readlink -f $0)"
|
||||
path="${path%/*/*}"
|
||||
path="${path##*/}"
|
||||
if [[ "$path" == "i3" ]]; then
|
||||
workSpace="$(i3-msg -t get_workspaces \
|
||||
| jq '.[] | select(.focused==true).name' \
|
||||
| cut -d"\"" -f2)"
|
||||
else
|
||||
workSpace="$(swaymsg -t get_workspaces \
|
||||
| jq '.[] | select(.focused==true).name' \
|
||||
| cut -d"\"" -f2)"
|
||||
fi
|
||||
left=9
|
||||
right=0
|
||||
msg="Workspace ${workSpace}"
|
||||
if [[ "${workSpace}" == "11" ]]; then
|
||||
play -qnV0 synth 1.5 pl A4 pl E5 pl C5 delay 0.0 0.1 0.2 remix - fade p 0 1.5 .5
|
||||
spd-say -P important -Cw "I38 panel"
|
||||
exit 0
|
||||
fi
|
||||
if ! [[ "${workSpace}" =~ ^[0-9]+$ ]]; then
|
||||
right=9
|
||||
else
|
||||
|
36
scripts/battery_alert.sh
Executable file
36
scripts/battery_alert.sh
Executable file
@ -0,0 +1,36 @@
|
||||
#!/bin/sh
|
||||
|
||||
# This file is part of I38.
|
||||
|
||||
# I38 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation,
|
||||
# either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
# I38 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
# PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with I38. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
if ! command -v acpi &> /dev/null ; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
while : ; do
|
||||
acpi -b | awk -F'[,:%]' '{print $2, $3}' | {
|
||||
read -r status capacity
|
||||
|
||||
if [[ "$status" == "Discharging" ]] && [[ "$capacity" -le 15 ]] && [[ "$capacity" -gt 10 ]]; then
|
||||
play -qV0 "|sox -n -p synth saw E2 fade 0 0.25 0.05" "|sox -n -p synth saw E2 fade 0 0.25 0.05" norm -7
|
||||
spd-say -P important "Battery $capacity percent."
|
||||
elif [[ "$status" == "Discharging" ]] && [[ "$capacity" -le 10 ]] && [[ "$capacity" -gt 5 ]]; then
|
||||
play -qV0 "|sox -n -p synth saw E2 fade 0 0.25 0.05" "|sox -n -p synth saw E2 fade 0 0.25 0.05" norm -7
|
||||
spd-say -P important "Battery $capacity percent."
|
||||
elif [[ "$status" == "Discharging" ]] && [[ "$capacity" -lt 5 ]]; then
|
||||
play -qV0 "|sox -np synth sq C#5 sq D#5 sq F#5 sq A5 sq C#6" remix - fade 0 5.5 5 pitch -400
|
||||
spd-say -P important "Battery $capacity percent."
|
||||
fi
|
||||
}
|
||||
sleep 5m
|
||||
done
|
||||
|
||||
exit 0
|
@ -1,5 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This file is part of I38.
|
||||
|
||||
# I38 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation,
|
||||
# either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
# I38 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
# PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with I38. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
#check for acpi
|
||||
if command -v acpi &> /dev/null; then
|
||||
bat="$(acpi -b)"
|
||||
|
32
scripts/bind_to_scratchpad.sh
Executable file
32
scripts/bind_to_scratchpad.sh
Executable file
@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Find out if we're using i3
|
||||
if ! [[ -n "${WAYLAND_DISPLAY}" ]]; then
|
||||
cmd="i3-msg"
|
||||
scratchConfig="${XDG_CONFIG_HOME:-$HOME/.config}/i3"
|
||||
else
|
||||
cmd="swaymsg"
|
||||
scratchConfig="${XDG_CONFIG_HOME:-$HOME/.config}/sway"
|
||||
fi
|
||||
scratchConfig+="/scratchpad"
|
||||
touch "${scratchConfig}"
|
||||
|
||||
# Get the focused window ID
|
||||
windowId=$(xdotool getactivewindow)
|
||||
|
||||
# Get the class name of the window
|
||||
class=$(xprop -id "$windowId" WM_CLASS | awk -F '"' '{print $4}')
|
||||
|
||||
if [[ -z "$class" ]]; then
|
||||
notify-send "Unable to move to scratchpad."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if it's already in the config
|
||||
if ! grep -q "class=\"$class\"" "$scratchConfig"; then
|
||||
echo "for_window [class=\"$class\"] move to scratchpad" >> "$scratchConfig"
|
||||
notify-send "Added window class $class to scratchpad"
|
||||
fi
|
||||
|
||||
# Move the window to scratchpad now
|
||||
$cmd "[class=\"$class\"] move to scratchpad"
|
142
scripts/bookmarks.sh
Executable file
142
scripts/bookmarks.sh
Executable file
@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This file is part of I38.
|
||||
|
||||
# I38 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation,
|
||||
# either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
# I38 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
# PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with I38. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
add_bookmark() {
|
||||
while : ; do
|
||||
if [[ -r "${config}/surfraw/bookmarks" ]] && [[ -r "${config}/i3/bookmarks" ]]; then
|
||||
# Ask for the Bookmark Title, URL, and file selection using yad --form with radio buttons
|
||||
input=$(yad --form \
|
||||
--title="I38 - Add Bookmark" \
|
||||
--text="New Bookmark:" \
|
||||
--field="Bookmark Title" \
|
||||
--field="URL" \
|
||||
--field="!Save As:CB" \
|
||||
--field="Surfraw Bookmark":CHK \
|
||||
--field="I38 Bookmark":CHK \
|
||||
--button="OK:0" \
|
||||
--button="Cancel:1" \
|
||||
--selectable-labels \
|
||||
--separator="|")
|
||||
else
|
||||
# Ask for the Bookmark Title and URL using yad --form without radio buttons
|
||||
input=$(yad --form \
|
||||
--title="I38 - Add Bookmark" \
|
||||
--text="New Bookmark:" \
|
||||
--field="Bookmark Title" \
|
||||
--field="URL" \
|
||||
--button="OK:0" \
|
||||
--button="Cancel:1" \
|
||||
--selectable-labels \
|
||||
--separator="|")
|
||||
fi
|
||||
yadCode="$?"
|
||||
|
||||
# Check if the user pressed Cancel
|
||||
if [[ $yadCode -eq 1 ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Split the input into components
|
||||
IFS='|' read -r title url bookmarkFile <<< "$input"
|
||||
|
||||
# Check if title or URL is empty
|
||||
if [[ -z "$title" || -z "$url" ]]; then
|
||||
yad --info --title="Add Bookmark" --text="Both Title and URL cannot be empty. Please try again."
|
||||
continue
|
||||
fi
|
||||
|
||||
# Check for spaces in title and prompt user
|
||||
if [[ "$title" =~ \ ]] && [[ "$bookmarkFile" == "Surfraw Bookmark" ]]; then
|
||||
newTitle="${title//[[:space:]]/_}"
|
||||
yad --question --title="I38 - Bookmarks" --text="The title contains spaces, which have been converted to underscores: \"$newTitle\". Is this okay?"
|
||||
yadCode="$?"
|
||||
if [[ $yadCode -ne 0 ]]; then
|
||||
continue
|
||||
else
|
||||
title="$newTitle"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Determine the bookmarks file based on radio button selection
|
||||
if [[ -f "${config}/surfraw/bookmarks" ]] && [[ -f "${config}/i3/bookmarks" ]]; then
|
||||
if [[ "$bookmarkFile" == "Surfraw Bookmark" ]]; then
|
||||
bookmarksFile="${config}/surfraw/bookmarks"
|
||||
else
|
||||
bookmarksFile="${config}/i3/bookmarks"
|
||||
fi
|
||||
else
|
||||
bookmarksFile="${config}/i3/bookmarks"
|
||||
fi
|
||||
|
||||
# Check for duplicates
|
||||
if grep -q "$url" "$bookmarksFile" ; then
|
||||
existingTitle=$(grep "$url" "$bookmarksFile" | cut -d' ' -f1)
|
||||
yad --form --title="I38 - Bookmarks" --selectable-labels --field="This site is already bookmarked as \"$existingTitle\".":LBL --button="OK:0"
|
||||
return
|
||||
elif grep -qi "^$title " "$bookmarksFile"; then
|
||||
yad --form --title="I38 - Bookmarks" --selectable-labels --field="A bookmark with the title \"$title\" already exists.":LBL --button="OK:0"
|
||||
return
|
||||
fi
|
||||
|
||||
# Add the new bookmark
|
||||
echo "$title $url" >> "$bookmarksFile"
|
||||
|
||||
# Ensure no duplicates with sort -u
|
||||
sort -u "$bookmarksFile" -o "$bookmarksFile"
|
||||
yad --form --title="I38 - Bookmarks" --selectable-labels --field="Bookmark \"$title\" added successfully.":LBL --button="OK:0"
|
||||
return
|
||||
done
|
||||
}
|
||||
|
||||
# Prepare environment and read bookmarks
|
||||
declare -a bookmarks
|
||||
config="${XDG_CONFIG_HOME:-${HOME}/.config}"
|
||||
if [[ -r "${config}/surfraw/bookmarks" ]]; then
|
||||
mapfile -t contents < "${config}/surfraw/bookmarks"
|
||||
for i in "${contents[@]}" ; do
|
||||
title="${i% *}"
|
||||
title="${title//_/ }"
|
||||
bookmarks+=("${title}")
|
||||
bookmarks+=("${i##* }")
|
||||
done
|
||||
fi
|
||||
if [[ -r "${config}/i3/bookmarks" ]]; then
|
||||
mapfile -t contents < "${config}/i3/bookmarks"
|
||||
for i in "${contents[@]}" ; do
|
||||
bookmarks+=("${i% *}" "${i##* }")
|
||||
done
|
||||
fi
|
||||
|
||||
# Run yad to display the dialog
|
||||
url=$(yad --list \
|
||||
--title="I38 - Bookmarks" \
|
||||
--text="Select a bookmark to open" \
|
||||
--column="Title" \
|
||||
--column="URL" \
|
||||
--button="Open Bookmark:0" \
|
||||
--button="Add Bookmark:1" \
|
||||
--button="Cancel:2" \
|
||||
--close-on-unfocus \
|
||||
--hide-column=2 \
|
||||
--search-column=1 \
|
||||
--skip-taskbar \
|
||||
"${bookmarks[@]}")
|
||||
yadCode="$?"
|
||||
url="${url%|*}"
|
||||
url="${url##*|}"
|
||||
case ${yadCode} in
|
||||
0) xdg-open "${url}";;
|
||||
1) add_bookmark;;
|
||||
2) exit 0;;
|
||||
esac
|
||||
|
51
scripts/desktop.sh
Executable file
51
scripts/desktop.sh
Executable file
@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This file is part of I38.
|
||||
|
||||
# I38 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation,
|
||||
# either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
# I38 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
# PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with I38. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# Make sure there is a desktop to search.
|
||||
if [[ ! -d ~/Desktop ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Discover directories and files on the desktop
|
||||
mapfile -t desktopDirs < <(find ~/Desktop -mindepth 1 -maxdepth 1 -type d)
|
||||
mapfile -t desktopFiles < <(find ~/Desktop -mindepth 1 -maxdepth 1 -type f)
|
||||
|
||||
# Combine directories and files into one list
|
||||
desktopItems=("${desktopDirs[@]}" "${desktopFiles[@]}")
|
||||
|
||||
# Build menu for yad.
|
||||
declare -a menuList
|
||||
for i in "${desktopItems[@]}" ; do
|
||||
case "$i" in
|
||||
*".desktop")
|
||||
menuList+=("$(grep "^Name" "$i" | cut -f2- -d '=')" "gio launch \"$i\"")
|
||||
;;
|
||||
*)
|
||||
menuList+=("${i##*/}" "/usr/bin/env xdg-open \"$i\"")
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
launch=$(yad --list \
|
||||
--title="I38 Desktop" \
|
||||
--column="Items" \
|
||||
--column="Launch Command" \
|
||||
--close-on-unfocus \
|
||||
--hide-column=2 \
|
||||
--search-column=1 \
|
||||
--skip-taskbar \
|
||||
"${menuList[@]}")
|
||||
if [[ $? -eq 0 ]]; then
|
||||
launch="${launch%|}"
|
||||
launch="${launch##*|}"
|
||||
eval "$launch"
|
||||
fi
|
@ -1,5 +1,16 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This file is part of I38.
|
||||
|
||||
# I38 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation,
|
||||
# either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
# I38 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
# PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with I38. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
# Add this to your crontab to have battery status automatically reported when it starts getting low
|
||||
# */10 * * * * XDG_RUNTIME_DIR=/run/user/1000 /home/user/.config/i3/scripts/game_controler.sh
|
||||
|
||||
@ -8,7 +19,7 @@
|
||||
batteryName=""
|
||||
|
||||
if [[ "$batteryName" == "" ]]; then
|
||||
batteryName="$(find /sys/class/power_supply -name 'sony_controller_battery_*' | cut -d/ -f5)"
|
||||
batteryName="$(find /sys/class/power_supply -name 'sony_controller_battery_*' -or -name 'ps-controller-battery-*' | cut -d/ -f5)"
|
||||
fi
|
||||
|
||||
# If there's no file, we don't check it.
|
23
scripts/i3-sensible-terminal.sh
Executable file
23
scripts/i3-sensible-terminal.sh
Executable file
@ -0,0 +1,23 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# This code is released in public domain by Han Boetes <han@mijncomputer.nl>
|
||||
#
|
||||
# This script tries to exec a terminal emulator by trying some known terminal
|
||||
# emulators.
|
||||
#
|
||||
# We welcome patches that add distribution-specific mechanisms to find the
|
||||
# preferred terminal emulator. On Debian, there is the x-terminal-emulator
|
||||
# symlink for example.
|
||||
#
|
||||
# Invariants:
|
||||
# 1. $TERMINAL must come first
|
||||
# 2. Distribution-specific mechanisms come next, e.g. x-terminal-emulator
|
||||
# 3. The terminal emulator with best accessibility comes first.
|
||||
# 4. No order is guaranteed/desired for the remaining terminal emulators.
|
||||
for terminal in "$TERMINAL" x-terminal-emulator mate-terminal gnome-terminal terminator xfce4-terminal urxvt rxvt termit Eterm aterm uxterm xterm roxterm termite lxterminal terminology st qterminal lilyterm tilix terminix konsole kitty guake tilda alacritty hyper wezterm; do
|
||||
if command -v "$terminal" > /dev/null 2>&1; then
|
||||
exec "$terminal" "$@"
|
||||
fi
|
||||
done
|
||||
|
||||
i3-nagbar -m 'i3-sensible-terminal could not find a terminal emulator. Please install one.'
|
@ -1,6 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
|
||||
# This file is part of I38.
|
||||
|
||||
# I38 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation,
|
||||
# either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
# I38 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
# PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with I38. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
configPath="$(readlink -f $0)"
|
||||
configPath="${configPath%/*/*}"
|
||||
|
||||
|
57
scripts/keyboard.sh
Executable file
57
scripts/keyboard.sh
Executable file
@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This file is part of I38.
|
||||
|
||||
# I38 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation,
|
||||
# either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
# I38 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
# PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with I38. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
# This script is a modified version of i3-keyboard-layout.
|
||||
# Originally Copyright (c) 2018 Sergio Gil.
|
||||
# https://github.com/porras/i3-keyboard-layout
|
||||
# The original work was released under the MIT license.
|
||||
|
||||
|
||||
set -e
|
||||
|
||||
get_kbdlayout() {
|
||||
layout=$(setxkbmap -query | grep -oP 'layout:\s*\K([\w,]+)')
|
||||
variant=$(setxkbmap -query | grep -oP 'variant:\s*\K(\w+)')
|
||||
echo "$layout" "$variant"
|
||||
}
|
||||
|
||||
set_kbdlayout() {
|
||||
eval "array=($1)"
|
||||
setxkbmap "${array[@]}" &&
|
||||
spd-say -P important -Cw "${array[@]}"
|
||||
}
|
||||
|
||||
cycle() {
|
||||
current_layout=$(get_kbdlayout | xargs)
|
||||
layouts=("$@" "$1") # add the first one at the end so that it cycles
|
||||
index=0
|
||||
while [ "${layouts[$index]}" != "$current_layout" ] && [ $index -lt "${#layouts[@]}" ]; do index=$[index +1]; done
|
||||
next_index=$[index +1]
|
||||
next_layout=${layouts[$next_index]}
|
||||
set_kbdlayout "$next_layout"
|
||||
}
|
||||
|
||||
|
||||
subcommand="$1"
|
||||
shift || exit 1
|
||||
|
||||
case $subcommand in
|
||||
"get")
|
||||
echo -n $(get_kbdlayout)
|
||||
;;
|
||||
"cycle")
|
||||
cycle "$@"
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
15
scripts/launch_remind.sh
Executable file
15
scripts/launch_remind.sh
Executable file
@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This file is part of I38.
|
||||
|
||||
# I38 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation,
|
||||
# either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
# I38 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
# PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with I38. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
pgrep remind > /dev/null 2>&1 && exit 0
|
||||
command remind -z '-k:${HOME}/.config/i3/scripts/reminder.sh %s &' ${HOME}/.reminders < /dev/null > /dev/null 2>&1 &
|
659
scripts/menu.py
Executable file
659
scripts/menu.py
Executable file
@ -0,0 +1,659 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# This file is part of I38.
|
||||
|
||||
# I38 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation,
|
||||
# either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
# I38 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
# PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with I38. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import configparser
|
||||
from pathlib import Path
|
||||
from collections import defaultdict
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
gi.require_version('Atk', '1.0')
|
||||
from gi.repository import Gtk, Gdk, GLib, Atk
|
||||
|
||||
def read_desktop_files(paths):
|
||||
desktopEntries = []
|
||||
for path in paths:
|
||||
if not Path(path).exists():
|
||||
continue
|
||||
for file in Path(path).rglob('*.desktop'):
|
||||
try:
|
||||
config = configparser.ConfigParser(interpolation=None)
|
||||
config.read(file, encoding='utf-8')
|
||||
desktopEntries.append(config)
|
||||
except (UnicodeDecodeError, configparser.Error):
|
||||
continue
|
||||
return desktopEntries
|
||||
|
||||
userApplicationsPath = Path.home() / '.local/share/applications'
|
||||
systemApplicationsPath = Path('/usr/share/applications')
|
||||
userFlatpakApplicationsPath = Path.home() / '.local/share/flatpak/exports/share/applications'
|
||||
systemFlatpakApplicationsPath = Path('/var/lib/flatpak/exports/share/applications')
|
||||
desktopEntries = read_desktop_files([userApplicationsPath, systemApplicationsPath, userFlatpakApplicationsPath, systemFlatpakApplicationsPath])
|
||||
|
||||
# Combine some of the categories
|
||||
categoryMap = {
|
||||
"2DGraphics": "Office",
|
||||
"Accessibility": "Settings",
|
||||
"Audio": "Audio and Video",
|
||||
"AudioVideo": "Audio and Video",
|
||||
"AudioVideoEditing": "Audio and Video",
|
||||
"ActionGame": "Games",
|
||||
"Building": "Development",
|
||||
"Calendar": "Office",
|
||||
"Chat": "Communication",
|
||||
"Database": "Office",
|
||||
"DesktopSettings": "Settings",
|
||||
"Emulator": "Games",
|
||||
"FlowChart": "Office",
|
||||
"Game": "Games",
|
||||
"HardwareSettings": "Settings",
|
||||
"IDE": "Development",
|
||||
"InstantMessaging": "Communication",
|
||||
"Math": "Education",
|
||||
"Midi": "Audio and Video",
|
||||
"Mixer": "Audio and Video",
|
||||
"Player": "Audio and Video",
|
||||
"Presentation": "Office",
|
||||
"Recorder": "Audio and Video",
|
||||
"Science": "Education",
|
||||
"Spreadsheet": "Office",
|
||||
"Telephony": "Communication",
|
||||
"Terminal": "Utilities",
|
||||
"TerminalEmulator": "Utilities",
|
||||
"TV": "Audio and Video",
|
||||
"Utility": "Utilities",
|
||||
"VectorGraphics": "Office",
|
||||
"Video": "Audio and Video",
|
||||
"WebDevelopment": "Development",
|
||||
"WordProcessor": "Office",
|
||||
"X-Alsa": "Audio and Video",
|
||||
"X-Fedora": "Utilities",
|
||||
"X-Jack": "Audio and Video",
|
||||
"X-LXDE-Settings": "Settings",
|
||||
"X-MATE-NetworkSettings": "Settings",
|
||||
"X-MATE-PersonalSettings": "Settings",
|
||||
"X-Red-Hat-Base": "Utilities",
|
||||
"X-SuSE-Core-Office": "Office",
|
||||
"X-XFCE": "XFCE",
|
||||
"X-XFCE-HardwareSettings": "Settings",
|
||||
"X-XFCE-PersonalSettings": "Settings",
|
||||
"X-XFCE-SettingsDialog": "Settings",
|
||||
"X-Xfce": "XFCE",
|
||||
"X-Xfce-Toplevel": "XFCE",
|
||||
}
|
||||
|
||||
# First, gather all applications by category
|
||||
categoryApps = defaultdict(dict)
|
||||
subcategories = defaultdict(set)
|
||||
|
||||
for entry in desktopEntries:
|
||||
try:
|
||||
# Check if NoDisplay=true is set
|
||||
if entry.getboolean('Desktop Entry', 'NoDisplay', fallback=False):
|
||||
continue
|
||||
|
||||
name = entry.get('Desktop Entry', 'Name', fallback=None)
|
||||
execCommand = entry.get('Desktop Entry', 'Exec', fallback=None)
|
||||
|
||||
if not name or not execCommand:
|
||||
continue
|
||||
|
||||
entryCategories = entry.get('Desktop Entry', 'Categories', fallback='').split(';')
|
||||
|
||||
# For applications with categories
|
||||
mainCategory = None
|
||||
validCategories = [cat for cat in entryCategories if cat.strip()]
|
||||
|
||||
if validCategories:
|
||||
# Use first valid category as main
|
||||
mainCategory = categoryMap.get(validCategories[0], validCategories[0])
|
||||
categoryApps[mainCategory][name] = execCommand
|
||||
else:
|
||||
# If no category was found, add to "Other"
|
||||
categoryApps["Other"][name] = execCommand
|
||||
|
||||
except (configparser.NoOptionError, KeyError):
|
||||
continue
|
||||
|
||||
# Ensure we have an "All Applications" category that contains everything
|
||||
allApps = {}
|
||||
for category, apps in categoryApps.items():
|
||||
allApps.update(apps)
|
||||
categoryApps["All Applications"] = allApps
|
||||
|
||||
class I38_Tab_Menu(Gtk.Window):
|
||||
def __init__(self):
|
||||
super().__init__(title="I38 Menu")
|
||||
self.set_default_size(500, 400)
|
||||
self.set_border_width(10)
|
||||
|
||||
# Main container
|
||||
self.mainBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
||||
self.add(self.mainBox)
|
||||
|
||||
# Add search box at the top
|
||||
self.searchBox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
||||
|
||||
# Create completion for the search entry
|
||||
self.completionStore = Gtk.ListStore(str, str) # Name, Exec
|
||||
self.completion = Gtk.EntryCompletion()
|
||||
self.completion.set_model(self.completionStore)
|
||||
self.completion.set_text_column(0)
|
||||
self.completion.set_minimum_key_length(1)
|
||||
self.completion.set_popup_completion(True)
|
||||
self.completion.set_inline_completion(False)
|
||||
self.completion.connect("match-selected", self.on_completion_match)
|
||||
|
||||
self.searchEntry = Gtk.Entry()
|
||||
self.searchEntry.set_completion(self.completion)
|
||||
self.searchEntry.set_placeholder_text("Search applications...")
|
||||
self.searchEntry.connect("changed", self.on_search_changed)
|
||||
self.searchEntry.connect("key-press-event", self.on_search_key_press)
|
||||
self.searchBox.pack_start(self.searchEntry, True, True, 0)
|
||||
|
||||
# Add search button for visual users
|
||||
self.searchButton = Gtk.Button.new_from_icon_name("search", Gtk.IconSize.BUTTON)
|
||||
self.searchButton.connect("clicked", self.on_search_activated)
|
||||
self.searchBox.pack_start(self.searchButton, False, False, 0)
|
||||
|
||||
self.mainBox.pack_start(self.searchBox, False, False, 0)
|
||||
|
||||
# Create notebook (tabbed interface)
|
||||
self.notebook = Gtk.Notebook()
|
||||
self.notebook.set_tab_pos(Gtk.PositionType.TOP)
|
||||
self.mainBox.pack_start(self.notebook, True, True, 0)
|
||||
|
||||
# Flag for search mode
|
||||
self.inSearchMode = False
|
||||
|
||||
# For incremental letter navigation
|
||||
self.typedText = ""
|
||||
self.typedTextTimer = None
|
||||
|
||||
# Add a tab for each major category
|
||||
self.treeViews = {} # Store TreeViews for each tab
|
||||
self.stores = {} # Store models for each tab
|
||||
|
||||
# Sort categories alphabetically, but ensure "All Applications" is first
|
||||
sortedCategories = sorted(categoryApps.keys())
|
||||
if "All Applications" in sortedCategories:
|
||||
sortedCategories.remove("All Applications")
|
||||
sortedCategories.insert(0, "All Applications")
|
||||
|
||||
# Create tabs - defer TreeView creation for performance
|
||||
self.tabCategories = sortedCategories
|
||||
self.createdTabs = set() # Track which tabs have been created
|
||||
|
||||
for i, category in enumerate(sortedCategories):
|
||||
if not categoryApps[category]: # Skip empty categories
|
||||
continue
|
||||
|
||||
# Create placeholder scrolled window
|
||||
scrolledWindow = Gtk.ScrolledWindow()
|
||||
scrolledWindow.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
||||
|
||||
# Create tab label
|
||||
tabLabel = Gtk.Label(label=category)
|
||||
|
||||
# Add the tab
|
||||
self.notebook.append_page(scrolledWindow, tabLabel)
|
||||
|
||||
# Set tab accessibility properties for screen readers
|
||||
accessible = scrolledWindow.get_accessible()
|
||||
accessible.set_name(f"{category} applications")
|
||||
accessible.set_role(Atk.Role.LIST)
|
||||
|
||||
# Create first tab immediately
|
||||
if sortedCategories:
|
||||
self.create_tab_content(0)
|
||||
|
||||
# Connect notebook signals
|
||||
self.notebook.connect("switch-page", self.on_switch_page)
|
||||
|
||||
# Connect window signals
|
||||
self.connect("key-press-event", self.on_window_key_press)
|
||||
self.connect("focus-out-event", self.on_focus_out)
|
||||
|
||||
# Add all applications to the completion store
|
||||
self.populate_completion_store()
|
||||
|
||||
# Set accessibility properties
|
||||
windowAccessible = self.get_accessible()
|
||||
windowAccessible.set_name("I38 Application Menu")
|
||||
windowAccessible.set_description("Tab-based application launcher menu. Press slash to search, type app name and use down arrow to navigate results. Type letters to incrementally navigate to matching applications.")
|
||||
|
||||
def create_tab_content(self, tabIndex):
|
||||
"""Create TreeView content for a tab on demand"""
|
||||
if tabIndex in self.createdTabs or tabIndex >= len(self.tabCategories):
|
||||
return
|
||||
|
||||
category = self.tabCategories[tabIndex]
|
||||
if not categoryApps[category]:
|
||||
return
|
||||
|
||||
# Create a TreeStore for this category
|
||||
store = Gtk.TreeStore(str, str) # Columns: Name, Exec
|
||||
self.stores[category] = store
|
||||
|
||||
# Add applications to this category's store (simplified - no subcategories)
|
||||
sortedApps = sorted(categoryApps[category].items())
|
||||
for appName, appExec in sortedApps:
|
||||
store.append(None, [appName, appExec])
|
||||
|
||||
# Create TreeView for this category
|
||||
treeView = Gtk.TreeView(model=store)
|
||||
treeView.set_headers_visible(False)
|
||||
self.treeViews[category] = treeView
|
||||
|
||||
# Add column for application names
|
||||
renderer = Gtk.CellRendererText()
|
||||
column = Gtk.TreeViewColumn("Applications", renderer, text=0)
|
||||
treeView.append_column(column)
|
||||
|
||||
# Get the scrolled window for this tab
|
||||
scrolledWindow = self.notebook.get_nth_page(tabIndex)
|
||||
scrolledWindow.add(treeView)
|
||||
|
||||
# Connect signals
|
||||
treeView.connect("row-activated", self.on_row_activated)
|
||||
treeView.connect("key-press-event", self.on_key_press)
|
||||
|
||||
# Show the new content
|
||||
treeView.show()
|
||||
|
||||
# Mark this tab as created
|
||||
self.createdTabs.add(tabIndex)
|
||||
|
||||
def populate_completion_store(self):
|
||||
"""Populate completion store with all available applications"""
|
||||
self.completionStore.clear()
|
||||
|
||||
# Add all applications from all categories
|
||||
for tabName, appDict in categoryApps.items():
|
||||
for appName, execCommand in sorted(appDict.items()):
|
||||
self.completionStore.append([appName, execCommand])
|
||||
|
||||
def on_switch_page(self, notebook, page, pageNum):
|
||||
# Create tab content if not already created
|
||||
self.create_tab_content(pageNum)
|
||||
|
||||
# Focus the TreeView in the newly selected tab
|
||||
tab = notebook.get_nth_page(pageNum)
|
||||
for child in tab.get_children():
|
||||
if isinstance(child, Gtk.TreeView):
|
||||
child.grab_focus()
|
||||
break
|
||||
|
||||
def on_row_activated(self, treeView, path, column):
|
||||
model = treeView.get_model()
|
||||
treeIter = model.get_iter(path)
|
||||
execCommand = model.get_value(treeIter, 1)
|
||||
|
||||
if execCommand:
|
||||
# Launch the application
|
||||
cmdParts = execCommand.split()
|
||||
# Remove field codes like %f, %F, %u, %U
|
||||
cleanCmd = [p for p in cmdParts if not (p.startswith('%') and len(p) == 2)]
|
||||
cleanCmd = ' '.join(cleanCmd)
|
||||
|
||||
# Use GLib.spawn_command_line_async for better process handling
|
||||
try:
|
||||
GLib.spawn_command_line_async(cleanCmd)
|
||||
Gtk.main_quit()
|
||||
except GLib.Error as e:
|
||||
dialog = Gtk.MessageDialog(
|
||||
transient_for=self,
|
||||
flags=0,
|
||||
message_type=Gtk.MessageType.ERROR,
|
||||
buttons=Gtk.ButtonsType.OK,
|
||||
text=f"Failed to launch application: {e.message}"
|
||||
)
|
||||
dialog.run()
|
||||
dialog.destroy()
|
||||
|
||||
def on_completion_match(self, completion, model, iterator):
|
||||
"""Handle when a completion item is selected"""
|
||||
appName = model[iterator][0]
|
||||
execCommand = model[iterator][1]
|
||||
|
||||
if execCommand:
|
||||
# Launch the application
|
||||
cmdParts = execCommand.split()
|
||||
# Remove field codes like %f, %F, %u, %U
|
||||
cleanCmd = [p for p in cmdParts if not (p.startswith('%') and len(p) == 2)]
|
||||
cleanCmd = ' '.join(cleanCmd)
|
||||
|
||||
# Use GLib.spawn_command_line_async for better process handling
|
||||
try:
|
||||
GLib.spawn_command_line_async(cleanCmd)
|
||||
Gtk.main_quit()
|
||||
except GLib.Error as e:
|
||||
dialog = Gtk.MessageDialog(
|
||||
transient_for=self,
|
||||
flags=0,
|
||||
message_type=Gtk.MessageType.ERROR,
|
||||
buttons=Gtk.ButtonsType.OK,
|
||||
text=f"Failed to launch application: {e.message}"
|
||||
)
|
||||
dialog.run()
|
||||
dialog.destroy()
|
||||
|
||||
return True
|
||||
|
||||
def on_search_changed(self, entry):
|
||||
"""Handle search text changes"""
|
||||
searchText = entry.get_text().lower()
|
||||
self.inSearchMode = bool(searchText)
|
||||
|
||||
# If entering search mode, make sure "All Applications" tab is active
|
||||
if self.inSearchMode and not self.notebook.get_current_page() == 0:
|
||||
self.notebook.set_current_page(0)
|
||||
|
||||
currentTab = self.notebook.get_current_page()
|
||||
tabName = list(self.treeViews.keys())[currentTab]
|
||||
treeView = self.treeViews[tabName]
|
||||
store = self.stores[tabName]
|
||||
|
||||
self.filter_applications(store, searchText)
|
||||
|
||||
# Update the completion model with filtered results
|
||||
self.update_completion_results(searchText)
|
||||
|
||||
# Set accessibility announcement
|
||||
if self.inSearchMode:
|
||||
treeView.get_accessible().set_name(f"Search results for {searchText}")
|
||||
else:
|
||||
treeView.get_accessible().set_name(f"{tabName} applications")
|
||||
|
||||
def update_completion_results(self, searchText):
|
||||
"""Update completion dropdown with matching applications"""
|
||||
if not searchText:
|
||||
# If empty, restore all applications
|
||||
self.populate_completion_store()
|
||||
return
|
||||
|
||||
# Clear and repopulate with filtered results
|
||||
self.completionStore.clear()
|
||||
|
||||
for tabName, appDict in categoryApps.items():
|
||||
for appName, execCommand in sorted(appDict.items()):
|
||||
if searchText.lower() in appName.lower():
|
||||
self.completionStore.append([appName, execCommand])
|
||||
|
||||
def filter_applications(self, store, searchText):
|
||||
"""Show/hide applications based on search text"""
|
||||
def filterRow(model, iterator):
|
||||
if not self.inSearchMode:
|
||||
return True
|
||||
|
||||
appName = model.get_value(iterator, 0).lower()
|
||||
|
||||
# Always show categories (rows with no exec command)
|
||||
execCommand = model.get_value(iterator, 1)
|
||||
if execCommand is None:
|
||||
# Check if any children match
|
||||
childIter = model.iter_children(iterator)
|
||||
while childIter:
|
||||
childName = model.get_value(childIter, 0).lower()
|
||||
if searchText in childName:
|
||||
return True
|
||||
childIter = model.iter_next(childIter)
|
||||
return False
|
||||
|
||||
# Show app if it matches search
|
||||
return searchText in appName
|
||||
|
||||
for tabName, treeStore in self.stores.items():
|
||||
# Replace store with filtered version
|
||||
filterStore = treeStore.filter_new()
|
||||
filterStore.set_visible_func(filterRow)
|
||||
|
||||
treeView = self.treeViews[tabName]
|
||||
treeView.set_model(filterStore)
|
||||
|
||||
# Expand all categories in search mode
|
||||
if self.inSearchMode:
|
||||
treeView.expand_all()
|
||||
else:
|
||||
treeView.collapse_all()
|
||||
|
||||
def on_search_key_press(self, entry, event):
|
||||
"""Handle keyboard input in search box"""
|
||||
keyval = event.keyval
|
||||
|
||||
if keyval == Gdk.KEY_Down:
|
||||
# Check if we can navigate to completion results
|
||||
if self.completion.get_popup_completion() and self.inSearchMode:
|
||||
# Try to activate completion popup and navigate to it
|
||||
self.completion.complete()
|
||||
return True
|
||||
|
||||
# If not in completion mode, move focus to the treeview
|
||||
currentTab = self.notebook.get_current_page()
|
||||
tabName = list(self.treeViews.keys())[currentTab]
|
||||
treeView = self.treeViews[tabName]
|
||||
treeView.grab_focus()
|
||||
|
||||
# Set cursor to first item
|
||||
model = treeView.get_model()
|
||||
iterator = model.get_iter_first()
|
||||
if iterator:
|
||||
path = model.get_path(iterator)
|
||||
treeView.set_cursor(path)
|
||||
return True
|
||||
|
||||
elif keyval == Gdk.KEY_Escape:
|
||||
if self.searchEntry.get_text():
|
||||
# Clear search if there's text
|
||||
self.searchEntry.set_text("")
|
||||
return True
|
||||
else:
|
||||
# Otherwise close the app
|
||||
Gtk.main_quit()
|
||||
return True
|
||||
|
||||
# Other keys pass through
|
||||
return False
|
||||
|
||||
def on_search_activated(self, widget):
|
||||
"""Process search when search button is clicked"""
|
||||
searchText = self.searchEntry.get_text()
|
||||
if searchText:
|
||||
# Focus on the first result if any
|
||||
currentTab = self.notebook.get_current_page()
|
||||
tabName = list(self.treeViews.keys())[currentTab]
|
||||
treeView = self.treeViews[tabName]
|
||||
model = treeView.get_model()
|
||||
|
||||
# Find first visible row
|
||||
iterator = model.get_iter_first()
|
||||
if iterator:
|
||||
path = model.get_path(iterator)
|
||||
treeView.set_cursor(path)
|
||||
treeView.grab_focus()
|
||||
|
||||
def on_key_press(self, widget, event):
|
||||
keyval = event.keyval
|
||||
|
||||
if keyval == Gdk.KEY_Escape:
|
||||
# Reset incremental typing and close app if no ongoing search
|
||||
if self.typedText:
|
||||
self.typedText = ""
|
||||
# Announce reset
|
||||
current_tab = self.notebook.get_current_page()
|
||||
tab_name = list(self.treeViews.keys())[current_tab]
|
||||
treeView = self.treeViews[tab_name]
|
||||
treeView.get_accessible().set_name(f"Type reset. {tab_name} applications")
|
||||
return True
|
||||
else:
|
||||
Gtk.main_quit()
|
||||
return True
|
||||
|
||||
elif keyval == Gdk.KEY_slash:
|
||||
# Forward slash activates search
|
||||
self.searchEntry.grab_focus()
|
||||
return True
|
||||
|
||||
elif keyval == Gdk.KEY_Left:
|
||||
# If in a TreeView and Left key is pressed
|
||||
path = widget.get_cursor()[0]
|
||||
if path:
|
||||
model = widget.get_model()
|
||||
treeIter = model.get_iter(path)
|
||||
|
||||
if widget.row_expanded(path):
|
||||
# Collapse the current row if it's expanded
|
||||
widget.collapse_row(path)
|
||||
return True
|
||||
else:
|
||||
# Move to parent if possible
|
||||
parentIter = model.iter_parent(treeIter)
|
||||
if parentIter:
|
||||
parentPath = model.get_path(parentIter)
|
||||
widget.set_cursor(parentPath)
|
||||
return True
|
||||
|
||||
# If we couldn't handle it in the TreeView, try to switch tabs
|
||||
currentPage = self.notebook.get_current_page()
|
||||
if currentPage > 0 and not self.inSearchMode:
|
||||
self.notebook.set_current_page(currentPage - 1)
|
||||
return True
|
||||
|
||||
elif keyval == Gdk.KEY_Right:
|
||||
# If in a TreeView and Right key is pressed
|
||||
path = widget.get_cursor()[0]
|
||||
if path:
|
||||
model = widget.get_model()
|
||||
treeIter = model.get_iter(path)
|
||||
|
||||
# Check if this row has children
|
||||
if model.iter_has_child(treeIter):
|
||||
if not widget.row_expanded(path):
|
||||
widget.expand_row(path, False)
|
||||
# Move to the first child
|
||||
childPath = model.get_path(model.iter_children(treeIter))
|
||||
widget.set_cursor(childPath)
|
||||
return True
|
||||
|
||||
# If we couldn't handle it in the TreeView, try to switch tabs
|
||||
currentPage = self.notebook.get_current_page()
|
||||
if currentPage < self.notebook.get_n_pages() - 1 and not self.inSearchMode:
|
||||
self.notebook.set_current_page(currentPage + 1)
|
||||
return True
|
||||
|
||||
elif keyval == Gdk.KEY_Tab:
|
||||
# Control tab navigation
|
||||
currentPage = self.notebook.get_current_page()
|
||||
if event.state & Gdk.ModifierType.SHIFT_MASK:
|
||||
# Shift+Tab -> previous tab
|
||||
if currentPage > 0 and not self.inSearchMode:
|
||||
self.notebook.set_current_page(currentPage - 1)
|
||||
else:
|
||||
self.notebook.set_current_page(self.notebook.get_n_pages() - 1)
|
||||
else:
|
||||
# Tab -> next tab
|
||||
if currentPage < self.notebook.get_n_pages() - 1 and not self.inSearchMode:
|
||||
self.notebook.set_current_page(currentPage + 1)
|
||||
else:
|
||||
self.notebook.set_current_page(0)
|
||||
return True
|
||||
|
||||
# Incremental letter navigation
|
||||
elif Gdk.KEY_a <= keyval <= Gdk.KEY_z or Gdk.KEY_A <= keyval <= Gdk.KEY_Z:
|
||||
# Cancel any pending timer
|
||||
if self.typedTextTimer:
|
||||
GLib.source_remove(self.typedTextTimer)
|
||||
self.typedTextTimer = None
|
||||
|
||||
# Add the new letter to the typed text
|
||||
letter = chr(keyval).lower()
|
||||
self.typedText += letter
|
||||
|
||||
# Find item matching typed text
|
||||
found = self.find_incremental_match(widget, self.typedText)
|
||||
|
||||
# Set timer to reset typed text after 1.5 seconds of inactivity
|
||||
self.typedTextTimer = GLib.timeout_add(1500, self.reset_typed_text)
|
||||
|
||||
# Announce the letters being typed
|
||||
current_tab = self.notebook.get_current_page()
|
||||
tab_name = list(self.treeViews.keys())[current_tab]
|
||||
treeView = self.treeViews[tab_name]
|
||||
treeView.get_accessible().set_name(f"Typed: {self.typedText}")
|
||||
|
||||
return True if found else False
|
||||
|
||||
return False
|
||||
|
||||
def reset_typed_text(self):
|
||||
"""Reset the typed text after timeout"""
|
||||
self.typedText = ""
|
||||
self.typedTextTimer = None
|
||||
return False # Don't call again
|
||||
|
||||
def find_incremental_match(self, treeView, text):
|
||||
"""Find items matching the incrementally typed text"""
|
||||
if not text:
|
||||
return False
|
||||
|
||||
model = treeView.get_model()
|
||||
found = False
|
||||
|
||||
def search_tree(model, path, treeIter, user_data):
|
||||
nonlocal found
|
||||
if found:
|
||||
return True # Stop iteration if already found
|
||||
|
||||
name = model.get_value(treeIter, 0)
|
||||
if name and name.lower().startswith(text.lower()):
|
||||
# Found a match
|
||||
treeView.set_cursor(path)
|
||||
treeView.scroll_to_cell(path, None, True, 0.5, 0.5)
|
||||
found = True
|
||||
return True # Stop iteration
|
||||
return False # Continue iteration
|
||||
|
||||
# Search the entire model
|
||||
model.foreach(search_tree, None)
|
||||
return found
|
||||
|
||||
def on_window_key_press(self, widget, event):
|
||||
# Handle window-level key events
|
||||
keyval = event.keyval
|
||||
|
||||
if keyval == Gdk.KEY_Escape:
|
||||
Gtk.main_quit()
|
||||
return True
|
||||
|
||||
elif keyval == Gdk.KEY_slash:
|
||||
# Forward slash activates search
|
||||
self.searchEntry.grab_focus()
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def on_focus_out(self, widget, event):
|
||||
# Quit when the window loses focus
|
||||
Gtk.main_quit()
|
||||
|
||||
window = I38_Tab_Menu()
|
||||
window.connect("destroy", Gtk.main_quit)
|
||||
window.show_all()
|
||||
|
||||
# Focus the first TreeView
|
||||
firstTab = window.notebook.get_nth_page(0)
|
||||
for child in firstTab.get_children():
|
||||
if isinstance(child, Gtk.TreeView):
|
||||
child.grab_focus()
|
||||
break
|
||||
|
||||
Gtk.main()
|
@ -1,6 +1,16 @@
|
||||
#!/bin/bash
|
||||
|
||||
|
||||
# This file is part of I38.
|
||||
|
||||
# I38 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation,
|
||||
# either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
# I38 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
# PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with I38. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
run_command() {
|
||||
playerctl -p '%any,chromium,firefox' $1
|
||||
sleep 0.25
|
||||
|
@ -1,9 +1,21 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This file is part of I38.
|
||||
|
||||
# I38 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation,
|
||||
# either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
# I38 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
# PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with I38. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
if [ $(pamixer --get-mute) = false ]; then
|
||||
spd-say -P important -Cw 'Muting!'
|
||||
pamixer -t
|
||||
else
|
||||
pamixer -t
|
||||
play -qnG synth 0.05 sin 440
|
||||
play -qnGV0 synth 0.05 sin 440
|
||||
spd-say -P important -Cw 'Unmuted!'
|
||||
fi
|
||||
|
823
scripts/notes.py
Executable file
823
scripts/notes.py
Executable file
@ -0,0 +1,823 @@
|
||||
#!/usr/bin/env python3
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, GLib, Gio, Gdk
|
||||
import os
|
||||
import datetime
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
class JotApp(Gtk.Application):
|
||||
def __init__(self):
|
||||
super().__init__(application_id="com.example.jot",
|
||||
flags=Gio.ApplicationFlags.FLAGS_NONE)
|
||||
|
||||
# Set up data structures
|
||||
self.dbPath = self.get_db_path()
|
||||
self.conn = None
|
||||
self.init_database()
|
||||
|
||||
# Initialize settings
|
||||
self.expirationDays = self.get_setting("expirationDays", 0)
|
||||
self.confirmDelete = self.get_setting("confirmDelete", 1)
|
||||
|
||||
def do_activate(self):
|
||||
# Create main window when app is activated
|
||||
self.window = Gtk.ApplicationWindow(application=self, title="I38 Notes")
|
||||
self.window.set_default_size(500, 400)
|
||||
|
||||
# Connect the delete-event signal (for window close button)
|
||||
self.window.connect("delete-event", self.on_window_close)
|
||||
|
||||
# Set up keyboard shortcuts
|
||||
self.setup_actions()
|
||||
|
||||
# Build the main interface
|
||||
self.build_ui()
|
||||
|
||||
# Check for expired notes
|
||||
self.check_expirations()
|
||||
|
||||
self.window.show_all()
|
||||
|
||||
def on_window_close(self, window, event):
|
||||
"""Handle window close event"""
|
||||
# Close the database connection before quitting
|
||||
if self.conn:
|
||||
self.conn.close()
|
||||
self.quit()
|
||||
return True
|
||||
|
||||
def get_db_path(self):
|
||||
"""Get path to the SQLite database"""
|
||||
configHome = os.environ.get('XDG_CONFIG_HOME', os.path.join(os.path.expanduser('~'), '.config'))
|
||||
configDir = os.path.join(configHome, 'stormux', 'I38')
|
||||
os.makedirs(configDir, exist_ok=True)
|
||||
return os.path.join(configDir, 'notes.sqlite')
|
||||
|
||||
def init_database(self):
|
||||
"""Initialize the SQLite database"""
|
||||
try:
|
||||
self.conn = sqlite3.connect(self.dbPath)
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
# Create notes table if it doesn't exist
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS notes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
text TEXT NOT NULL,
|
||||
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
expires TIMESTAMP NULL,
|
||||
locked BOOLEAN DEFAULT 0
|
||||
)
|
||||
''')
|
||||
|
||||
# Create settings table if it doesn't exist
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
)
|
||||
''')
|
||||
|
||||
self.conn.commit()
|
||||
|
||||
# Initialize default settings if they don't exist
|
||||
self.init_default_settings()
|
||||
|
||||
# Check if we need to migrate from old format
|
||||
self.migrate_if_needed()
|
||||
|
||||
except sqlite3.Error as e:
|
||||
print(f"Database error: {e}")
|
||||
|
||||
def init_default_settings(self):
|
||||
"""Initialize default settings if they don't exist"""
|
||||
defaultSettings = {
|
||||
"expirationDays": "0",
|
||||
"confirmDelete": "1" # 1 = enabled, 0 = disabled
|
||||
}
|
||||
|
||||
cursor = self.conn.cursor()
|
||||
for key, value in defaultSettings.items():
|
||||
cursor.execute(
|
||||
"INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)",
|
||||
(key, value)
|
||||
)
|
||||
self.conn.commit()
|
||||
|
||||
def migrate_if_needed(self):
|
||||
"""Check if we need to migrate from old format"""
|
||||
# Check for old config directory
|
||||
oldConfigHome = os.environ.get('XDG_CONFIG_HOME', os.path.join(os.path.expanduser('~'), '.config'))
|
||||
oldConfigDir = os.path.join(oldConfigHome, 'jot')
|
||||
oldNotesFile = os.path.join(oldConfigDir, 'notes')
|
||||
|
||||
if os.path.exists(oldNotesFile):
|
||||
try:
|
||||
# Check if we already have notes in the database
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute("SELECT COUNT(*) FROM notes")
|
||||
count = cursor.fetchone()[0]
|
||||
|
||||
# Only migrate if database is empty
|
||||
if count == 0:
|
||||
with open(oldNotesFile, 'r') as f:
|
||||
for line in f:
|
||||
parts = line.strip().split(': ', 1)
|
||||
if len(parts) == 2:
|
||||
noteNum, noteText = parts
|
||||
cursor.execute(
|
||||
"INSERT INTO notes (text, locked) VALUES (?, ?)",
|
||||
(noteText, 0)
|
||||
)
|
||||
|
||||
self.conn.commit()
|
||||
print(f"Migrated notes from {oldNotesFile}")
|
||||
except Exception as e:
|
||||
print(f"Migration error: {e}")
|
||||
|
||||
def get_setting(self, key, default=None):
|
||||
"""Get a setting from the database"""
|
||||
try:
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute("SELECT value FROM settings WHERE key = ?", (key,))
|
||||
result = cursor.fetchone()
|
||||
|
||||
if result:
|
||||
return result[0]
|
||||
else:
|
||||
# Set default if not exists
|
||||
self.save_setting(key, default)
|
||||
return default
|
||||
except sqlite3.Error:
|
||||
return default
|
||||
|
||||
def save_setting(self, key, value):
|
||||
"""Save a setting to the database"""
|
||||
try:
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute(
|
||||
"INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)",
|
||||
(key, value)
|
||||
)
|
||||
self.conn.commit()
|
||||
except sqlite3.Error as e:
|
||||
print(f"Settings error: {e}")
|
||||
|
||||
def check_expirations(self):
|
||||
"""Check and remove expired notes"""
|
||||
try:
|
||||
cursor = self.conn.cursor()
|
||||
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# Find expired notes that aren't locked
|
||||
cursor.execute(
|
||||
"SELECT id, text FROM notes WHERE expires IS NOT NULL AND expires < ? AND locked = 0",
|
||||
(now,)
|
||||
)
|
||||
expired = cursor.fetchall()
|
||||
|
||||
if expired:
|
||||
# Delete expired notes
|
||||
cursor.execute(
|
||||
"DELETE FROM notes WHERE expires IS NOT NULL AND expires < ? AND locked = 0",
|
||||
(now,)
|
||||
)
|
||||
self.conn.commit()
|
||||
|
||||
expiredCount = len(expired)
|
||||
if expiredCount > 0:
|
||||
self.show_status_message(f"Removed {expiredCount} expired notes")
|
||||
# Refresh notes list
|
||||
self.populate_notes()
|
||||
except sqlite3.Error as e:
|
||||
print(f"Expiration check error: {e}")
|
||||
|
||||
def build_ui(self):
|
||||
"""Build the main user interface with tabs"""
|
||||
# Create notebook (tabbed interface)
|
||||
self.notebook = Gtk.Notebook()
|
||||
self.notebook.set_tab_pos(Gtk.PositionType.TOP)
|
||||
|
||||
# Make tabs keyboard navigable
|
||||
self.notebook.set_can_focus(True)
|
||||
|
||||
self.window.add(self.notebook)
|
||||
|
||||
# Build notes tab
|
||||
notesTab = self.build_notes_tab()
|
||||
notesTabLabel = Gtk.Label(label="Notes")
|
||||
self.notebook.append_page(notesTab, notesTabLabel)
|
||||
self.notebook.set_tab_reorderable(notesTab, False)
|
||||
|
||||
# Build settings tab
|
||||
settingsTab = self.build_settings_tab()
|
||||
settingsTabLabel = Gtk.Label(label="Settings")
|
||||
self.notebook.append_page(settingsTab, settingsTabLabel)
|
||||
self.notebook.set_tab_reorderable(settingsTab, False)
|
||||
|
||||
# Connect tab change signal
|
||||
self.notebook.connect("switch-page", self.on_tab_switched)
|
||||
|
||||
def build_notes_tab(self):
|
||||
"""Build the notes tab"""
|
||||
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
||||
vbox.set_border_width(10)
|
||||
|
||||
# Notes list with scrolling
|
||||
scrolled = Gtk.ScrolledWindow()
|
||||
scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
|
||||
|
||||
# Create list store and view
|
||||
self.notesStore = Gtk.ListStore(int, str, bool, str) # id, text, locked, expiry
|
||||
self.notesView = Gtk.TreeView(model=self.notesStore)
|
||||
self.notesView.set_activate_on_single_click(False)
|
||||
self.notesView.connect("row-activated", self.on_row_activated)
|
||||
|
||||
# Improve keyboard navigation in the tree view
|
||||
self.notesView.set_can_focus(True)
|
||||
self.notesView.set_headers_clickable(True)
|
||||
self.notesView.set_enable_search(True)
|
||||
self.notesView.set_search_column(1) # Search by note text
|
||||
|
||||
# Add columns with renderers
|
||||
self.add_columns()
|
||||
|
||||
# Populate the list
|
||||
self.populate_notes()
|
||||
|
||||
scrolled.add(self.notesView)
|
||||
vbox.pack_start(scrolled, True, True, 0)
|
||||
|
||||
# Action buttons
|
||||
actionBox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
||||
|
||||
# Copy button
|
||||
copyButton = Gtk.Button.new_with_label("Copy to Clipboard")
|
||||
copyButton.connect("clicked", self.on_copy_clicked)
|
||||
copyButton.set_can_focus(True)
|
||||
actionBox.pack_start(copyButton, False, False, 0)
|
||||
|
||||
# Delete button
|
||||
deleteButton = Gtk.Button.new_with_label("Delete Note")
|
||||
deleteButton.connect("clicked", self.on_delete_button_clicked)
|
||||
deleteButton.set_can_focus(True)
|
||||
actionBox.pack_start(deleteButton, False, False, 0)
|
||||
|
||||
vbox.pack_start(actionBox, False, False, 0)
|
||||
|
||||
# Entry for adding new notes
|
||||
entryBox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
||||
self.newNoteEntry = Gtk.Entry()
|
||||
self.newNoteEntry.set_placeholder_text("Type a new note and press Enter")
|
||||
self.newNoteEntry.connect("activate", self.on_entry_activate)
|
||||
self.newNoteEntry.set_can_focus(True)
|
||||
entryBox.pack_start(self.newNoteEntry, True, True, 0)
|
||||
|
||||
# Add button
|
||||
addButton = Gtk.Button.new_with_label("Add Note")
|
||||
addButton.connect("clicked", self.on_add_clicked)
|
||||
addButton.set_can_focus(True)
|
||||
entryBox.pack_start(addButton, False, False, 0)
|
||||
|
||||
vbox.pack_start(entryBox, False, False, 0)
|
||||
|
||||
# Status bar
|
||||
self.statusbar = Gtk.Statusbar()
|
||||
self.statusbarCtx = self.statusbar.get_context_id("jot")
|
||||
vbox.pack_start(self.statusbar, False, False, 0)
|
||||
|
||||
return vbox
|
||||
|
||||
def build_settings_tab(self):
|
||||
"""Build the settings tab"""
|
||||
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
|
||||
vbox.set_border_width(20)
|
||||
|
||||
# Create a frame for expiration settings
|
||||
expiryFrame = Gtk.Frame(label="Note Expiration")
|
||||
vbox.pack_start(expiryFrame, False, False, 0)
|
||||
|
||||
# Container for frame content
|
||||
expiryBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
|
||||
expiryBox.set_border_width(10)
|
||||
expiryFrame.add(expiryBox)
|
||||
|
||||
# Note expiration setting
|
||||
# First radio button for "Never expire"
|
||||
self.neverExpireRadio = Gtk.RadioButton.new_with_label_from_widget(None, "Never expire notes")
|
||||
self.neverExpireRadio.set_can_focus(True)
|
||||
expiryBox.pack_start(self.neverExpireRadio, False, False, 0)
|
||||
|
||||
# Container for expiration days selection
|
||||
expireDaysBox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
||||
|
||||
# Radio button for custom expiration
|
||||
self.customExpireRadio = Gtk.RadioButton.new_with_label_from_widget(
|
||||
self.neverExpireRadio,
|
||||
"Expire notes after"
|
||||
)
|
||||
self.customExpireRadio.set_can_focus(True)
|
||||
expireDaysBox.pack_start(self.customExpireRadio, False, False, 0)
|
||||
|
||||
# Spin button for days
|
||||
adjustment = Gtk.Adjustment(
|
||||
value=max(1, int(self.expirationDays)) if int(self.expirationDays) > 0 else 7,
|
||||
lower=1,
|
||||
upper=30,
|
||||
step_increment=1
|
||||
)
|
||||
self.daysSpinButton = Gtk.SpinButton()
|
||||
self.daysSpinButton.set_adjustment(adjustment)
|
||||
self.daysSpinButton.set_can_focus(True)
|
||||
expireDaysBox.pack_start(self.daysSpinButton, False, False, 0)
|
||||
|
||||
# Label for "days"
|
||||
daysLabel = Gtk.Label(label="days")
|
||||
expireDaysBox.pack_start(daysLabel, False, False, 0)
|
||||
|
||||
expiryBox.pack_start(expireDaysBox, False, False, 0)
|
||||
|
||||
# Create a frame for confirmation settings
|
||||
confirmFrame = Gtk.Frame(label="Confirmations")
|
||||
vbox.pack_start(confirmFrame, False, False, 10)
|
||||
|
||||
# Container for confirmation settings
|
||||
confirmBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
|
||||
confirmBox.set_border_width(10)
|
||||
confirmFrame.add(confirmBox)
|
||||
|
||||
# Delete confirmation checkbox
|
||||
self.confirmDeleteCheck = Gtk.CheckButton(label="Confirm before deleting notes")
|
||||
self.confirmDeleteCheck.set_active(bool(int(self.confirmDelete)))
|
||||
self.confirmDeleteCheck.set_can_focus(True)
|
||||
confirmBox.pack_start(self.confirmDeleteCheck, False, False, 0)
|
||||
|
||||
# Set the active radio button based on current setting
|
||||
if int(self.expirationDays) > 0:
|
||||
self.customExpireRadio.set_active(True)
|
||||
else:
|
||||
self.neverExpireRadio.set_active(True)
|
||||
|
||||
# Connect signals
|
||||
self.neverExpireRadio.connect("toggled", self.on_expiry_radio_toggled)
|
||||
self.customExpireRadio.connect("toggled", self.on_expiry_radio_toggled)
|
||||
|
||||
# Enable/disable the spin button based on the selected radio
|
||||
self.on_expiry_radio_toggled(None)
|
||||
|
||||
# Save button
|
||||
saveButton = Gtk.Button.new_with_label("Save Settings")
|
||||
saveButton.connect("clicked", self.on_save_settings)
|
||||
saveButton.set_can_focus(True)
|
||||
vbox.pack_start(saveButton, False, False, 10)
|
||||
|
||||
return vbox
|
||||
|
||||
def add_columns(self):
|
||||
"""Add columns to the TreeView"""
|
||||
# ID Column
|
||||
renderer = Gtk.CellRendererText()
|
||||
column = Gtk.TreeViewColumn("ID", renderer, text=0)
|
||||
column.set_sort_column_id(0)
|
||||
self.notesView.append_column(column)
|
||||
|
||||
# Note Text Column
|
||||
renderer = Gtk.CellRendererText()
|
||||
renderer.set_property("ellipsize", True)
|
||||
column = Gtk.TreeViewColumn("Note", renderer, text=1)
|
||||
column.set_expand(True)
|
||||
self.notesView.append_column(column)
|
||||
|
||||
# Locked Column
|
||||
renderer = Gtk.CellRendererToggle()
|
||||
renderer.connect("toggled", self.on_locked_toggled)
|
||||
column = Gtk.TreeViewColumn("Locked", renderer, active=2)
|
||||
self.notesView.append_column(column)
|
||||
|
||||
# Expiration Column
|
||||
renderer = Gtk.CellRendererText()
|
||||
column = Gtk.TreeViewColumn("Expires", renderer, text=3)
|
||||
self.notesView.append_column(column)
|
||||
|
||||
def populate_notes(self):
|
||||
"""Populate the list store with notes from the database"""
|
||||
self.notesStore.clear()
|
||||
|
||||
try:
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute(
|
||||
"SELECT id, text, locked, expires FROM notes ORDER BY id"
|
||||
)
|
||||
notes = cursor.fetchall()
|
||||
|
||||
for note in notes:
|
||||
noteId, text, locked, expires = note
|
||||
|
||||
# Format expiry date if it exists
|
||||
expiryText = ""
|
||||
if expires:
|
||||
try:
|
||||
expiryDate = datetime.datetime.strptime(expires, "%Y-%m-%d %H:%M:%S")
|
||||
expiryText = expiryDate.strftime("%Y-%m-%d")
|
||||
except:
|
||||
expiryText = "Invalid date"
|
||||
|
||||
self.notesStore.append([
|
||||
noteId, # Database ID
|
||||
text, # Note text
|
||||
bool(locked), # Locked status
|
||||
expiryText # Expiry date
|
||||
])
|
||||
except sqlite3.Error as e:
|
||||
print(f"Error populating notes: {e}")
|
||||
|
||||
def setup_actions(self):
|
||||
"""Set up keyboard shortcuts"""
|
||||
# Delete selected note
|
||||
deleteAction = Gio.SimpleAction.new("delete", None)
|
||||
deleteAction.connect("activate", self.on_delete_clicked)
|
||||
self.add_action(deleteAction)
|
||||
self.set_accels_for_action("app.delete", ["Delete"])
|
||||
|
||||
# Toggle lock on selected note
|
||||
lockAction = Gio.SimpleAction.new("lock", None)
|
||||
lockAction.connect("activate", self.on_lock_toggled)
|
||||
self.add_action(lockAction)
|
||||
self.set_accels_for_action("app.lock", ["l"])
|
||||
|
||||
# Copy note to clipboard
|
||||
copyAction = Gio.SimpleAction.new("copy", None)
|
||||
copyAction.connect("activate", self.on_copy_clicked)
|
||||
self.add_action(copyAction)
|
||||
self.set_accels_for_action("app.copy", ["<Control>c"])
|
||||
|
||||
# Switch to notes tab
|
||||
notesTabAction = Gio.SimpleAction.new("notes_tab", None)
|
||||
notesTabAction.connect("activate", lambda a, p: self.notebook.set_current_page(0))
|
||||
self.add_action(notesTabAction)
|
||||
self.set_accels_for_action("app.notes_tab", ["<Alt>1"])
|
||||
|
||||
# Switch to settings tab
|
||||
settingsTabAction = Gio.SimpleAction.new("settings_tab", None)
|
||||
settingsTabAction.connect("activate", lambda a, p: self.notebook.set_current_page(1))
|
||||
self.add_action(settingsTabAction)
|
||||
self.set_accels_for_action("app.settings_tab", ["<Alt>2"])
|
||||
|
||||
# Quit application
|
||||
quitAction = Gio.SimpleAction.new("quit", None)
|
||||
quitAction.connect("activate", lambda a, p: self.quit())
|
||||
self.add_action(quitAction)
|
||||
self.set_accels_for_action("app.quit", ["Escape"])
|
||||
|
||||
def show_status_message(self, message):
|
||||
"""Show a message in the statusbar"""
|
||||
self.statusbar.push(self.statusbarCtx, message)
|
||||
# Auto-remove after 5 seconds
|
||||
GLib.timeout_add_seconds(5, self.statusbar.pop, self.statusbarCtx)
|
||||
|
||||
def on_tab_switched(self, notebook, page, page_num):
|
||||
"""Handler for tab switching"""
|
||||
# Reset status bar on tab switch
|
||||
self.statusbar.pop(self.statusbarCtx)
|
||||
|
||||
# Set focus appropriately
|
||||
if page_num == 0: # Notes tab
|
||||
self.newNoteEntry.grab_focus()
|
||||
elif page_num == 1: # Settings tab
|
||||
if self.neverExpireRadio.get_active():
|
||||
self.neverExpireRadio.grab_focus()
|
||||
else:
|
||||
self.customExpireRadio.grab_focus()
|
||||
|
||||
def on_expiry_radio_toggled(self, widget):
|
||||
"""Handler for expiry radio button toggles"""
|
||||
# Enable/disable spin button based on which radio is active
|
||||
self.daysSpinButton.set_sensitive(self.customExpireRadio.get_active())
|
||||
|
||||
def on_save_settings(self, button):
|
||||
"""Handler for Save Settings button"""
|
||||
if self.neverExpireRadio.get_active():
|
||||
self.expirationDays = 0
|
||||
else:
|
||||
self.expirationDays = self.daysSpinButton.get_value_as_int()
|
||||
|
||||
# Get delete confirmation setting
|
||||
self.confirmDelete = 1 if self.confirmDeleteCheck.get_active() else 0
|
||||
|
||||
# Save to database
|
||||
self.save_setting("expirationDays", self.expirationDays)
|
||||
self.save_setting("confirmDelete", self.confirmDelete)
|
||||
|
||||
# Apply expiration to notes if needed
|
||||
if self.expirationDays > 0:
|
||||
try:
|
||||
cursor = self.conn.cursor()
|
||||
now = datetime.datetime.now()
|
||||
expiryDate = now + datetime.timedelta(days=self.expirationDays)
|
||||
expiryStr = expiryDate.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# Set expiration for notes that aren't locked and don't have expiration
|
||||
cursor.execute(
|
||||
"UPDATE notes SET expires = ? WHERE locked = 0 AND expires IS NULL",
|
||||
(expiryStr,)
|
||||
)
|
||||
self.conn.commit()
|
||||
|
||||
# Refresh the notes list
|
||||
self.populate_notes()
|
||||
except sqlite3.Error as e:
|
||||
print(f"Error updating expirations: {e}")
|
||||
|
||||
self.show_status_message("Settings saved")
|
||||
|
||||
# Switch back to notes tab
|
||||
self.notebook.set_current_page(0)
|
||||
|
||||
def on_entry_activate(self, entry):
|
||||
"""Handle Enter key in the entry field"""
|
||||
self.add_new_note(entry.get_text())
|
||||
entry.set_text("")
|
||||
|
||||
def on_add_clicked(self, button):
|
||||
"""Handle Add Note button click"""
|
||||
self.add_new_note(self.newNoteEntry.get_text())
|
||||
self.newNoteEntry.set_text("")
|
||||
|
||||
def add_new_note(self, text):
|
||||
"""Add a new note to the database"""
|
||||
if not text.strip():
|
||||
self.show_status_message("Note text cannot be empty")
|
||||
return
|
||||
|
||||
try:
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
# Set expiration if enabled
|
||||
expires = None
|
||||
if int(self.expirationDays) > 0:
|
||||
expiryDate = datetime.datetime.now() + datetime.timedelta(days=int(self.expirationDays))
|
||||
expires = expiryDate.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# Insert the new note
|
||||
cursor.execute(
|
||||
"INSERT INTO notes (text, expires, locked) VALUES (?, ?, ?)",
|
||||
(text, expires, 0)
|
||||
)
|
||||
self.conn.commit()
|
||||
|
||||
# Refresh the notes list
|
||||
self.populate_notes()
|
||||
self.show_status_message("Note added")
|
||||
except sqlite3.Error as e:
|
||||
self.show_status_message(f"Error adding note: {e}")
|
||||
|
||||
def on_row_activated(self, view, path, column):
|
||||
"""Handle double-click on a note - edit the note"""
|
||||
model = view.get_model()
|
||||
noteId = model[path][0]
|
||||
|
||||
# Get the note from the database
|
||||
try:
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute("SELECT text, locked, expires FROM notes WHERE id = ?", (noteId,))
|
||||
note = cursor.fetchone()
|
||||
|
||||
if note:
|
||||
self.edit_note_dialog(noteId, note)
|
||||
except sqlite3.Error as e:
|
||||
self.show_status_message(f"Error retrieving note: {e}")
|
||||
|
||||
def on_locked_toggled(self, renderer, path):
|
||||
"""Handle toggling the locked state from the view"""
|
||||
model = self.notesView.get_model()
|
||||
noteId = model[path][0]
|
||||
currentLocked = model[path][2]
|
||||
newLocked = not currentLocked
|
||||
|
||||
try:
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
# Update locked status
|
||||
cursor.execute(
|
||||
"UPDATE notes SET locked = ? WHERE id = ?",
|
||||
(1 if newLocked else 0, noteId)
|
||||
)
|
||||
|
||||
# If unlocking and expiration is enabled, set expiration
|
||||
if not newLocked and int(self.expirationDays) > 0:
|
||||
expiryDate = datetime.datetime.now() + datetime.timedelta(days=int(self.expirationDays))
|
||||
expiryStr = expiryDate.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
cursor.execute(
|
||||
"UPDATE notes SET expires = ? WHERE id = ?",
|
||||
(expiryStr, noteId)
|
||||
)
|
||||
|
||||
# Update the expiry text in the model
|
||||
model[path][3] = expiryDate.strftime("%Y-%m-%d")
|
||||
|
||||
self.show_status_message("Note unlocked - expiration set")
|
||||
elif newLocked:
|
||||
self.show_status_message("Note locked - will not expire")
|
||||
else:
|
||||
self.show_status_message("Note unlocked")
|
||||
|
||||
# Update the model
|
||||
model[path][2] = newLocked
|
||||
|
||||
self.conn.commit()
|
||||
except sqlite3.Error as e:
|
||||
self.show_status_message(f"Error updating note: {e}")
|
||||
|
||||
def on_lock_toggled(self, action, parameter):
|
||||
"""Handle keyboard shortcut to toggle lock"""
|
||||
selection = self.notesView.get_selection()
|
||||
model, treeiter = selection.get_selected()
|
||||
if treeiter:
|
||||
noteId = model[treeiter][0]
|
||||
currentLocked = model[treeiter][2]
|
||||
|
||||
# Simulate clicking the toggle
|
||||
path = model.get_path(treeiter)
|
||||
self.on_locked_toggled(None, path)
|
||||
|
||||
def on_copy_clicked(self, action=None, parameter=None):
|
||||
"""Copy the selected note to clipboard"""
|
||||
selection = self.notesView.get_selection()
|
||||
model, treeiter = selection.get_selected()
|
||||
if treeiter:
|
||||
noteText = model[treeiter][1]
|
||||
|
||||
# Get the clipboard
|
||||
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
|
||||
clipboard.set_text(noteText, -1)
|
||||
|
||||
self.show_status_message("Note copied to clipboard")
|
||||
else:
|
||||
self.show_status_message("No note selected")
|
||||
|
||||
def on_delete_button_clicked(self, button):
|
||||
"""Handle Delete button click"""
|
||||
selection = self.notesView.get_selection()
|
||||
model, treeiter = selection.get_selected()
|
||||
if treeiter:
|
||||
noteId = model[treeiter][0]
|
||||
noteText = model[treeiter][1]
|
||||
|
||||
if int(self.confirmDelete):
|
||||
self.confirm_delete_note(noteId, noteText)
|
||||
else:
|
||||
self.delete_note(noteId)
|
||||
|
||||
def on_delete_clicked(self, action, parameter):
|
||||
"""Handle Delete key to remove a note"""
|
||||
selection = self.notesView.get_selection()
|
||||
model, treeiter = selection.get_selected()
|
||||
if treeiter:
|
||||
noteId = model[treeiter][0]
|
||||
noteText = model[treeiter][1]
|
||||
|
||||
if int(self.confirmDelete):
|
||||
self.confirm_delete_note(noteId, noteText)
|
||||
else:
|
||||
self.delete_note(noteId)
|
||||
|
||||
def delete_note(self, noteId):
|
||||
"""Delete a note by ID without confirmation"""
|
||||
try:
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute("DELETE FROM notes WHERE id = ?", (noteId,))
|
||||
self.conn.commit()
|
||||
|
||||
self.populate_notes()
|
||||
self.show_status_message("Note deleted")
|
||||
except sqlite3.Error as e:
|
||||
self.show_status_message(f"Error deleting note: {e}")
|
||||
|
||||
def confirm_delete_note(self, noteId, noteText):
|
||||
"""Show confirmation dialog before deleting a note"""
|
||||
if len(noteText) > 30:
|
||||
noteText = noteText[:30] + "..."
|
||||
|
||||
dialog = Gtk.MessageDialog(
|
||||
transient_for=self.window,
|
||||
flags=0,
|
||||
message_type=Gtk.MessageType.QUESTION,
|
||||
buttons=Gtk.ButtonsType.YES_NO,
|
||||
text=f"Delete note: {noteText}?"
|
||||
)
|
||||
dialog.set_default_response(Gtk.ResponseType.NO)
|
||||
|
||||
response = dialog.run()
|
||||
if response == Gtk.ResponseType.YES:
|
||||
self.delete_note(noteId)
|
||||
|
||||
dialog.destroy()
|
||||
|
||||
def edit_note_dialog(self, noteId, note):
|
||||
"""Show dialog to edit a note"""
|
||||
text, locked, expires = note
|
||||
|
||||
dialog = Gtk.Dialog(
|
||||
title="Edit Note",
|
||||
parent=self.window,
|
||||
flags=0,
|
||||
buttons=(
|
||||
Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
|
||||
Gtk.STOCK_SAVE, Gtk.ResponseType.OK
|
||||
)
|
||||
)
|
||||
dialog.set_default_size(400, 200)
|
||||
|
||||
# Make the dialog accessible
|
||||
dialog.set_role("dialog")
|
||||
dialog.set_property("has-tooltip", True)
|
||||
|
||||
# Create a text view for the note
|
||||
entryBox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
|
||||
entryBox.set_border_width(10)
|
||||
|
||||
label = Gtk.Label(label="Edit note text:")
|
||||
label.set_halign(Gtk.Align.START)
|
||||
entryBox.pack_start(label, False, False, 0)
|
||||
|
||||
# Scrolled window for text view
|
||||
scrolled = Gtk.ScrolledWindow()
|
||||
scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
||||
scrolled.set_shadow_type(Gtk.ShadowType.IN)
|
||||
|
||||
# Text view for multi-line editing
|
||||
textBuffer = Gtk.TextBuffer()
|
||||
textBuffer.set_text(text)
|
||||
textView = Gtk.TextView.new_with_buffer(textBuffer)
|
||||
textView.set_wrap_mode(Gtk.WrapMode.WORD)
|
||||
textView.set_can_focus(True)
|
||||
|
||||
scrolled.add(textView)
|
||||
entryBox.pack_start(scrolled, True, True, 0)
|
||||
|
||||
# Add lock checkbox
|
||||
lockCheck = Gtk.CheckButton(label="Lock note (prevent expiration)")
|
||||
lockCheck.set_active(bool(locked))
|
||||
lockCheck.set_can_focus(True)
|
||||
entryBox.pack_start(lockCheck, False, False, 0)
|
||||
|
||||
# Show expiration date if it exists
|
||||
if expires and not locked:
|
||||
try:
|
||||
expiryDate = datetime.datetime.strptime(expires, "%Y-%m-%d %H:%M:%S")
|
||||
expiryLabel = Gtk.Label(label=f"Expires on: {expiryDate.strftime('%Y-%m-%d')}")
|
||||
expiryLabel.set_halign(Gtk.Align.START)
|
||||
entryBox.pack_start(expiryLabel, False, False, 0)
|
||||
except:
|
||||
pass
|
||||
|
||||
dialog.get_content_area().add(entryBox)
|
||||
dialog.set_default_response(Gtk.ResponseType.OK)
|
||||
dialog.show_all()
|
||||
|
||||
# Set focus to text view
|
||||
textView.grab_focus()
|
||||
|
||||
response = dialog.run()
|
||||
if response == Gtk.ResponseType.OK:
|
||||
# Get the text from the buffer
|
||||
start, end = textBuffer.get_bounds()
|
||||
newText = textBuffer.get_text(start, end, False)
|
||||
newLocked = lockCheck.get_active()
|
||||
|
||||
try:
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
# Update the note
|
||||
cursor.execute(
|
||||
"UPDATE notes SET text = ?, locked = ? WHERE id = ?",
|
||||
(newText, 1 if newLocked else 0, noteId)
|
||||
)
|
||||
|
||||
# Update expiration if needed
|
||||
if not newLocked and int(self.expirationDays) > 0:
|
||||
expiryDate = datetime.datetime.now() + datetime.timedelta(days=int(self.expirationDays))
|
||||
expiryStr = expiryDate.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
cursor.execute(
|
||||
"UPDATE notes SET expires = ? WHERE id = ?",
|
||||
(expiryStr, noteId)
|
||||
)
|
||||
|
||||
self.conn.commit()
|
||||
self.populate_notes()
|
||||
self.show_status_message("Note updated")
|
||||
except sqlite3.Error as e:
|
||||
self.show_status_message(f"Error updating note: {e}")
|
||||
|
||||
dialog.destroy()
|
||||
|
||||
def main():
|
||||
app = JotApp()
|
||||
return app.run(None)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
168
scripts/ocr.py
Executable file
168
scripts/ocr.py
Executable file
@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of I38.
|
||||
|
||||
# I38 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation,
|
||||
# either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
# I38 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
# PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with I38. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
"""
|
||||
Simple OCR Screen Reader
|
||||
A lightweight tool that performs OCR on the screen and speaks the results
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import subprocess
|
||||
from PIL import Image, ImageOps
|
||||
import pytesseract
|
||||
import pyperclip
|
||||
|
||||
def capture_screen(max_retries=3, initial_delay=0.2):
|
||||
"""
|
||||
Capture the screen using scrot with robust checking and retries
|
||||
|
||||
Args:
|
||||
max_retries: Maximum number of attempts to read the image
|
||||
initial_delay: Initial delay in seconds (will increase with retries)
|
||||
"""
|
||||
temp_file = "/tmp/ocr_capture.png"
|
||||
|
||||
try:
|
||||
# Capture the screen
|
||||
subprocess.run(["scrot", temp_file], check=True)
|
||||
|
||||
# Wait and retry approach with validity checking
|
||||
delay = initial_delay
|
||||
for attempt in range(max_retries):
|
||||
time.sleep(delay)
|
||||
|
||||
# Check if file exists and has content
|
||||
if os.path.exists(temp_file) and os.path.getsize(temp_file) > 0:
|
||||
try:
|
||||
# Try to verify the image is valid
|
||||
with Image.open(temp_file) as test_img:
|
||||
# Just accessing a property forces PIL to validate the image
|
||||
test_img.size
|
||||
|
||||
# If we get here, the image is valid
|
||||
return Image.open(temp_file)
|
||||
except (IOError, OSError) as e:
|
||||
# Image exists but isn't valid yet
|
||||
if attempt < max_retries - 1:
|
||||
# Increase delay exponentially for next attempt
|
||||
delay *= 2
|
||||
continue
|
||||
else:
|
||||
raise Exception(f"Image file exists but is not valid after {max_retries} attempts")
|
||||
|
||||
# File doesn't exist or is empty
|
||||
if attempt < max_retries - 1:
|
||||
# Increase delay exponentially for next attempt
|
||||
delay *= 2
|
||||
else:
|
||||
raise Exception(f"Screenshot file not created properly after {max_retries} attempts")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error capturing screen: {e}")
|
||||
raise
|
||||
finally:
|
||||
# Ensure file is removed even if an error occurs
|
||||
if os.path.exists(temp_file):
|
||||
os.remove(temp_file)
|
||||
|
||||
def process_image(img, scale_factor=1.5):
|
||||
"""Process the image to improve OCR accuracy"""
|
||||
# Scale the image to improve OCR
|
||||
if scale_factor != 1:
|
||||
width, height = img.size
|
||||
img = img.resize((int(width * scale_factor), int(height * scale_factor)),
|
||||
Image.Resampling.BICUBIC)
|
||||
|
||||
# Convert to grayscale for faster processing
|
||||
img = ImageOps.grayscale(img)
|
||||
|
||||
# Improve contrast for better text recognition
|
||||
img = ImageOps.autocontrast(img)
|
||||
|
||||
return img
|
||||
|
||||
def perform_ocr(img, lang='eng'):
|
||||
"""Perform OCR on the image"""
|
||||
# Use tessaract with optimized settings
|
||||
# --oem 1: Use LSTM OCR Engine
|
||||
# --psm 6: Assume a single uniform block of text
|
||||
text = pytesseract.image_to_string(img, lang=lang, config='--oem 1 --psm 6')
|
||||
|
||||
return text
|
||||
|
||||
def copy_to_clipboard(text):
|
||||
"""Copy text to clipboard using pyperclip"""
|
||||
try:
|
||||
# Filter out empty lines and clean up the text
|
||||
lines = [line.strip() for line in text.split('\n') if line.strip()]
|
||||
cleaned_text = '\n'.join(lines) # Preserve line breaks for clipboard
|
||||
|
||||
if cleaned_text:
|
||||
pyperclip.copy(cleaned_text)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"Error copying to clipboard: {e}")
|
||||
return False
|
||||
|
||||
def speak_text(text):
|
||||
"""Speak the text using speech-dispatcher"""
|
||||
# Filter out empty lines and clean up the text
|
||||
lines = [line.strip() for line in text.split('\n') if line.strip()]
|
||||
cleaned_text = ' '.join(lines)
|
||||
|
||||
# Use speech-dispatcher to speak the text
|
||||
if cleaned_text:
|
||||
subprocess.run(["spd-say", "-Cw", cleaned_text])
|
||||
else:
|
||||
subprocess.run(["spd-say", "-Cw", "No text detected"])
|
||||
|
||||
def main():
|
||||
# Limit tesseract thread usage to improve performance on Pi
|
||||
os.environ["OMP_THREAD_LIMIT"] = "4"
|
||||
|
||||
try:
|
||||
# Announce start
|
||||
subprocess.run(["spd-say", "-Cw", "performing OCR"])
|
||||
|
||||
# Capture screen
|
||||
img = capture_screen()
|
||||
|
||||
# Process image
|
||||
processed_img = process_image(img, scale_factor=1.5)
|
||||
|
||||
# Perform OCR
|
||||
text = perform_ocr(processed_img)
|
||||
|
||||
# Copy to clipboard
|
||||
clipboard_success = copy_to_clipboard(text)
|
||||
|
||||
# Speak the results
|
||||
speak_text(text)
|
||||
|
||||
except Exception as e:
|
||||
# Let the user know something went wrong
|
||||
error_msg = f"Error during OCR: {str(e)}"
|
||||
print(error_msg)
|
||||
try:
|
||||
subprocess.run(["spd-say", "-Cw", "OCR failed"])
|
||||
except:
|
||||
# If even speech fails, at least we tried
|
||||
pass
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
387
scripts/reminder.sh
Executable file
387
scripts/reminder.sh
Executable file
@ -0,0 +1,387 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This file is part of I38.
|
||||
|
||||
# I38 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation,
|
||||
# either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
# I38 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
# PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with I38. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
error() {
|
||||
yad --form --selectable-labels --title "I38 - Reminder Error" --field="${*}":lbl --button="Close!gtk-ok":0
|
||||
}
|
||||
|
||||
message() {
|
||||
yad --form --selectable-labels --title "I38 - Reminder" --field="${*}":lbl --button="Close!gtk-ok":0
|
||||
}
|
||||
|
||||
|
||||
add_reminder() {
|
||||
info="$(yad --form --selectable-labels \
|
||||
--title "I38 - New Reminder" \
|
||||
--field="Comment for ~/.reminders file":lbl "" \
|
||||
--field="Reminder Comment" "# Added by I38" \
|
||||
--field="Enter date in yyyy-mm-dd format:":lbl "" \
|
||||
--field="Date" "$(date '+%Y-%m-%d')" \
|
||||
--field="Reminder text:":lbl "" \
|
||||
--field="Reminder" "" \
|
||||
--field="Select Hour:":num '1!1..12' \
|
||||
--field="Select Minute:":num '0!0..59' \
|
||||
--field="Select AM or PM":cb 'AM!PM' \
|
||||
--button="Cancel!gtk-cancel:1" \
|
||||
--button="Create Reminder!gtk-ok:0")"
|
||||
if [[ $? -eq 1 || $? -eq 252 ]]; then
|
||||
return
|
||||
fi
|
||||
while [[ $info =~ \|\| ]]; do
|
||||
info="${info//||/|}"
|
||||
done
|
||||
info="${info#|#}"
|
||||
# Get information for reminder into an array
|
||||
IFS='|' read -a reminder <<< $info
|
||||
# Fix time to be 2 digits.
|
||||
[[ ${#reminder[3]} -eq 1 ]] && reminder[3]="0${reminder[3]}"
|
||||
[[ ${#reminder[4]} -eq 1 ]] && reminder[4]="0${reminder[4]}"
|
||||
# Check date format
|
||||
if ! [[ "${reminder[1]}" =~ ^[2-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]$ ]]; then
|
||||
error "Invalid date format given, addition canceled."
|
||||
return
|
||||
fi
|
||||
if [[ ${#reminder[2]} -lt 3 ]]; then
|
||||
error "No reminder text given, addition canceled."
|
||||
return
|
||||
fi
|
||||
# Add reminder
|
||||
if [[ "${reminder[0]}" != "# " ]]; then
|
||||
echo "# ${reminder[0]}" >> ~/.reminders
|
||||
fi
|
||||
echo "REM $(date -d "${reminder[1]}" '+%b %d %Y') AT ${reminder[3]}:${reminder[4]}${reminder[5]} +5 MSG ${reminder[2]} %2." >> ~/.reminders
|
||||
if [[ -N ~/.reminders ]]; then
|
||||
message "Reminder added."
|
||||
else
|
||||
error "Something went wrong. The reminder was not added."
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
add_custom_reminder() {
|
||||
info="$(yad --form --selectable-labels \
|
||||
--title "I38 - New Custom Reminder" \
|
||||
--field="Comment for ~/.reminders file":lbl "" \
|
||||
--field="Reminder Comment" "" \
|
||||
--field="Reminder entry. Remember for notifications, it must ccontain the +5 after the time and the %2. at the end of the MSG.":lbl "" \
|
||||
--field="Reminder" "REM at +5 MSG %2.")"
|
||||
if [[ $? -eq 1 || $? -eq 252 ]]; then
|
||||
return
|
||||
fi
|
||||
while [[ $info =~ \|\| ]]; do
|
||||
info="${info//||/|}"
|
||||
done
|
||||
info="${info#|#}"
|
||||
# Get information for reminder into an array
|
||||
IFS='|' read -a reminder <<< $info
|
||||
if [[ "${reminder[0]}" != "# " ]]; then
|
||||
echo "# ${reminder[0]}" >> ~/.reminders
|
||||
fi
|
||||
if [[ "${reminder[1]}" != "REM at +5 MSG %2." ]]; then
|
||||
echo "${reminder[1]}" >> ~/.reminders
|
||||
message "Custom reminder added."
|
||||
else
|
||||
error "No reminder text entered, No action taken."
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
add_daily_reminder() {
|
||||
info="$(yad --form --selectable-labels \
|
||||
--title "I38 - New Daily Reminder" \
|
||||
--field="Reminder Text" "" \
|
||||
--field="Select Hour:":num '1!1..12' \
|
||||
--field="Select Minute:":num '0!0..59' \
|
||||
--field="Select AM or PM":cb 'AM!PM' \
|
||||
--button="Cancel!gtk-cancel:1" \
|
||||
--button="Create Reminder!gtk-ok:0")"
|
||||
|
||||
# Properly handle window close events.
|
||||
if [[ $? -eq 1 || $? -eq 252 ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
# Get information for reminder into an array
|
||||
IFS='|' read -a reminder <<< $info
|
||||
|
||||
# Fix time to be 2 digits.
|
||||
[[ ${#reminder[1]} -eq 1 ]] && reminder[1]="0${reminder[1]}"
|
||||
[[ ${#reminder[2]} -eq 1 ]] && reminder[2]="0${reminder[2]}"
|
||||
|
||||
# Make sure we have reminder text
|
||||
if [[ ${#reminder[0]} -lt 3 ]]; then
|
||||
error "No reminder text given, addition canceled."
|
||||
return
|
||||
fi
|
||||
reminderEntry="REM AT ${reminder[1]}:${reminder[2]}${reminder[3]} +5 REPEAT daily MSG ${reminder[0]} %2."
|
||||
echo "# Added by I38." >> ~/.reminders
|
||||
echo "$reminderEntry" >> ~/.reminders
|
||||
if [[ -N ~/.reminders ]]; then
|
||||
message "Reminder added."
|
||||
else
|
||||
error "Something went wrong. The reminder was not added."
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
add_monthly_reminder() {
|
||||
info="$(yad --form --selectable-labels \
|
||||
--title "I38 - New Monthly Reminder" \
|
||||
--field="For dates over 28, some months may be skipped. If you want to be sure of the last day of the month, check the last day of month box.":lbl "" \
|
||||
--field="Reminder Text" "" \
|
||||
--field="Select Date:":num '1!1..31' \
|
||||
--field="Select Hour:":num '1!1..12' \
|
||||
--field="Select Minute:":num '0!0..59' \
|
||||
--field="Select AM or PM":cb 'AM!PM' \
|
||||
--field="Last day of month":chk "FALSE" \
|
||||
--button="Cancel!gtk-cancel:1" \
|
||||
--button="Create Reminder!gtk-ok:0")"
|
||||
|
||||
# Properly handle window close events.
|
||||
if [[ $? -eq 1 || $? -eq 252 ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
# Get information for reminder into an array
|
||||
IFS='|' read -a reminder <<< $info
|
||||
|
||||
# Fix time and date to be 2 digits.
|
||||
[[ ${#reminder[2]} -eq 1 ]] && reminder[2]="0${reminder[2]}"
|
||||
[[ ${#reminder[3]} -eq 1 ]] && reminder[3]="0${reminder[3]}"
|
||||
[[ ${#reminder[4]} -eq 1 ]] && reminder[4]="0${reminder[4]}"
|
||||
|
||||
# Make sure we have reminder text
|
||||
if [[ ${#reminder[1]} -lt 3 ]]; then
|
||||
error "No reminder text given, addition canceled."
|
||||
return
|
||||
fi
|
||||
if [[ "${reminder[6]}" == "FALSE" ]]; then
|
||||
reminderEntry="REM ${reminder[2]} "
|
||||
else
|
||||
reminderEntry="REM 1 -1 "
|
||||
fi
|
||||
reminderEntry+="AT ${reminder[3]}:${reminder[4]}${reminder[5]} +5 REPEAT monthly MSG ${reminder[1]} %2."
|
||||
echo "# Added by I38." >> ~/.reminders
|
||||
echo "$reminderEntry" >> ~/.reminders
|
||||
if [[ -N ~/.reminders ]]; then
|
||||
message "Reminder added."
|
||||
else
|
||||
error "Something went wrong. The reminder was not added."
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
add_weekly_reminder() {
|
||||
info="$(yad --form --selectable-labels \
|
||||
--title "I38 - New Weekly Reminder" \
|
||||
--field="Reminder Text" "" \
|
||||
--field="Select Days":lbl "" \
|
||||
--field="Sunday":chk "FALSE" \
|
||||
--field="Monday":chk "FALSE" \
|
||||
--field="Tuesday":chk "FALSE" \
|
||||
--field="Wednesday":chk "FALSE" \
|
||||
--field="Thursday":chk "FALSE" \
|
||||
--field="Friday":chk "FALSE" \
|
||||
--field="Saturday":chk "FALSE" \
|
||||
--field="Select Hour:":num '1!1..12' \
|
||||
--field="Select Minute:":num '0!0..59' \
|
||||
--field="Select AM or PM":cb 'AM!PM' \
|
||||
--button="Cancel!gtk-cancel:1" \
|
||||
--button="Create Reminder!gtk-ok:0")"
|
||||
|
||||
# Properly handle window close events.
|
||||
if [[ $? -eq 1 || $? -eq 252 ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
# Get information for reminder into an array
|
||||
IFS='|' read -a reminder <<< $info
|
||||
|
||||
# Fix time to be 2 digits.
|
||||
[[ ${#reminder[9]} -eq 1 ]] && reminder[9]="0${reminder[9]}"
|
||||
[[ ${#reminder[10]} -eq 1 ]] && reminder[10]="0${reminder[10]}"
|
||||
|
||||
# Change checked days into their name.
|
||||
reminder[2]="${reminder[2]/TRUE/Sun}"
|
||||
reminder[3]="${reminder[3]/TRUE/Mon}"
|
||||
reminder[4]="${reminder[4]/TRUE/Tue}"
|
||||
reminder[5]="${reminder[5]/TRUE/Wed}"
|
||||
reminder[6]="${reminder[6]/TRUE/Thu}"
|
||||
reminder[7]="${reminder[7]/TRUE/Fri}"
|
||||
reminder[8]="${reminder[8]/TRUE/Sat}"
|
||||
|
||||
# Make sure we have reminder text
|
||||
if [[ ${#reminder[0]} -lt 3 ]]; then
|
||||
error "No reminder text given, addition canceled."
|
||||
return
|
||||
fi
|
||||
reminderEntry="REM "
|
||||
noDays=1
|
||||
for ((i=2;i<=8;i++)) ; do
|
||||
if [[ "${reminder[i]}" != "FALSE" ]]; then
|
||||
reminderEntry+="${reminder[i]} "
|
||||
noDays=0
|
||||
fi
|
||||
done
|
||||
if [[ $noDays -eq 1 ]]; then
|
||||
error "No days were selected for the reminder. Nothing changed."
|
||||
return
|
||||
fi
|
||||
reminderEntry+="AT ${reminder[9]}:${reminder[10]}${reminder[11]} +5 REPEAT weekly MSG ${reminder[0]} %2."
|
||||
echo "# Added by I38." >> ~/.reminders
|
||||
echo "$reminderEntry" >> ~/.reminders
|
||||
if [[ -N ~/.reminders ]]; then
|
||||
message "Reminder added."
|
||||
else
|
||||
error "Something went wrong. The reminder was not added."
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
view_reminders() {
|
||||
if ! [[ -r ~/.reminders ]]; then
|
||||
error "No reminders found."
|
||||
return
|
||||
fi
|
||||
|
||||
mapfile -t lines < ~/.reminders
|
||||
# Create an empty array to store cleaned-up reminders
|
||||
yadMenu=()
|
||||
|
||||
# Iterate through the reminder lines and clean them up
|
||||
for i in "${lines[@]}"; do
|
||||
# Remove the "REM" prefix and leading/trailing spaces
|
||||
formattedLine="${i#*REM }"
|
||||
# Remove MSG from the output.
|
||||
formattedLine="${formattedLine/MSG /}"
|
||||
# remove the usually %2. from the end of the line, but accept any digit in case someone changes it.
|
||||
formattedLine="${formattedLine% %[[:digit:]].}"
|
||||
|
||||
# Add to the menu
|
||||
yadMenu+=("$formattedLine")
|
||||
done
|
||||
|
||||
# Display the reminders
|
||||
reminder="$(yad --list --title "I38 - Reminders" --text "Current reminders:" \
|
||||
--column "Reminder" "${yadMenu[@]}" \
|
||||
--button="Close!gtk-ok:1" --button="Delete!gtk-delete:0" --response=1)"
|
||||
if [[ $? -ne 0 ]]; then
|
||||
return
|
||||
fi
|
||||
if [[ "${reminder:0:1}" == "#" ]]; then
|
||||
error "Please select the actual reminder to be deleted, anything starting with # is only a comment. Nothing changed."
|
||||
return
|
||||
fi
|
||||
# Remove the | from the end of reminder
|
||||
reminder="${reminder%|}"
|
||||
# Find the index to remove from lines.
|
||||
for i in "${!yadMenu[@]}" ; do
|
||||
if [[ "${yadMenu[i]}" == "${reminder}" ]]; then
|
||||
# Delete selected reminder and possible preceeding comment.
|
||||
commentIndex=$((i - 1))
|
||||
if [[ "${lines[commentIndex]:0:1}" == "#" ]]; then
|
||||
unset lines[$commentIndex]
|
||||
fi
|
||||
unset lines[$i]
|
||||
message "Reminder deleted."
|
||||
printf "%s\n" "${lines[@]}" > ~/.reminders
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
view_today() {
|
||||
if ! [[ -r ~/.reminders ]]; then
|
||||
error "No reminders found."
|
||||
return
|
||||
fi
|
||||
|
||||
mapfile -t lines < <(rem | tr -s $'\n')
|
||||
# Display the reminders
|
||||
yad --list --title "I38 - Reminders" --text "Today's reminders:" \
|
||||
--column "Reminder" "${lines[@]}" \
|
||||
--button="Close!gtk-ok:0"
|
||||
}
|
||||
|
||||
|
||||
if ! command -v remind &> /dev/null ; then
|
||||
error "Please install remind. For notifications, please make sure to have notification-daemon and notify-send as well. Run i38.sh to regenerate your i3 config after the needed components are installed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $# -ne 0 ]]; then
|
||||
sox -ndqV0 synth .1 tri 600 norm -9 pad .05 repeat
|
||||
notification="$*"
|
||||
if [[ "${notification}" =~ "http" ]]; then
|
||||
url="${notification##*http}"
|
||||
url="${url%%[[:space:]]*}"
|
||||
url="http${url}"
|
||||
notification="${notification/${url}[[:space:]]/}"
|
||||
url="${url%[[:space:].?!]}"
|
||||
fi
|
||||
if command -v xclip &> /dev/null && [[ "${#url}" -gt 3 ]]; then
|
||||
echo "${url}" | xclip -selection clipboard
|
||||
notify-send --hint=int:transient:1 -t 500 -r 38 "${notification} The URL has been copied to the clipboard."
|
||||
else
|
||||
notify-send --hint=int:transient:1 -t 500 -r 38 "${*}"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
while : ; do
|
||||
action=$(yad --title "I38 - Reminders" --form \
|
||||
--button="_View Today's Reminders!gtk-info":3 \
|
||||
--button="_View All Reminders!gtk-info":4 \
|
||||
--button="_Add Reminder!gtk-edit":0 \
|
||||
--button="_Add Daily Reminder!gtk-edit":2 \
|
||||
--button="_Add Weekly Reminder!gtk-edit":5 \
|
||||
--button="Add Monthly Reminder!gtk-edit":6 \
|
||||
--button="Add Custom Reminder!gtk-edit":7 \
|
||||
--button="Close!gtk-cancel":1 \
|
||||
--separator="")
|
||||
|
||||
case $? in
|
||||
0)
|
||||
# Single reminder
|
||||
add_reminder
|
||||
;;
|
||||
1|252)
|
||||
# Handle "Close" button click and escape.
|
||||
exit 0
|
||||
;;
|
||||
2)
|
||||
# Handle "Add Daily Reminder" button click
|
||||
add_daily_reminder
|
||||
;;
|
||||
3)
|
||||
# View today's reminders
|
||||
view_today
|
||||
;;
|
||||
4)
|
||||
# View reminders
|
||||
view_reminders
|
||||
;;
|
||||
5)
|
||||
# Handle "Add Weekly Reminder" button click
|
||||
add_weekly_reminder
|
||||
;;
|
||||
6)
|
||||
# Handle "Add Monthly Reminder" button click
|
||||
add_monthly_reminder
|
||||
;;
|
||||
7)
|
||||
# Handle "Add Custom Reminder" button click
|
||||
add_custom_reminder
|
||||
;;
|
||||
esac
|
||||
done
|
@ -1,6 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
|
||||
# This file is part of I38.
|
||||
|
||||
# I38 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation,
|
||||
# either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
# I38 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
# PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with I38. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
write_history(){
|
||||
oldHistory="$(grep -v "$txt" "${historyPath}/.history" | head -n 49)"
|
||||
echo -e "$txt\n$oldHistory" | sed 's/^$//g' > "${historyPath}/.history"
|
||||
|
35
scripts/screen_controller.sh
Executable file
35
scripts/screen_controller.sh
Executable file
@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This file is part of I38.
|
||||
|
||||
# I38 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation,
|
||||
# either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
# I38 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
# PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with I38. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
# Not for multiple screens.
|
||||
# Get the name of the screen.
|
||||
screenName="$(xrandr --query | grep "connected" | cut -d ' ' -f1 | head -n 1)"
|
||||
|
||||
menuOptions=(
|
||||
"1.0" "Maximum Brightness"
|
||||
"0.75" "75 percent"
|
||||
"0.5" "50 percent"
|
||||
"0.25" "25 percent"
|
||||
"0" "Screen Curtain"
|
||||
)
|
||||
|
||||
brightness="$(yad --list --title "I38" --text "Set Screen Brightness" --columns 2 --hide-column 1 --column "" --column "Select" "${menuOptions[@]}")"
|
||||
|
||||
if [[ ${#brightness} -lt 1 ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
xrandr --output ${screenName} --brightness ${brightness%%|*} &&
|
||||
spd-say -P important -Cw "Screen set to ${brightness#*|}."
|
||||
|
||||
exit 0
|
@ -1,6 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
# Written by Storm Dragon, Jeremiah, and contributers.
|
||||
# Released under the terms of the WTFPL http://www.wtfpl.net
|
||||
|
||||
# This file is part of I38.
|
||||
|
||||
# I38 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation,
|
||||
# either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
# I38 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
# PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with I38. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import i3ipc
|
||||
from i3ipc import Event
|
||||
@ -10,14 +19,25 @@ i3 = i3ipc.Connection()
|
||||
|
||||
|
||||
def on_new_window(self,i3):
|
||||
system('play -n synth .25 sin 440:880 sin 480:920 remix - norm -3 pitch -500 &')
|
||||
if i3.container.name == 'xfce4-notifyd':
|
||||
system('play -nqV0 synth .05 sq 1800 tri 2400 delay 0 .03 remix - repeat 2 echo .55 0.7 20 1 norm -12 &')
|
||||
else:
|
||||
system('play -nqV0 synth .25 sin 440:880 sin 480:920 remix - norm -3 pitch -500 &')
|
||||
|
||||
def on_close_window(self,i3):
|
||||
if i3.container.name != 'xfce4-notifyd':
|
||||
system('play -nqV0 synth .25 sin 880:440 sin 920:480 remix - norm -3 pitch -500 &')
|
||||
|
||||
def on_mode(self,event):
|
||||
mode= event.change
|
||||
if mode == 'default':
|
||||
if mode == 'ratpoison':
|
||||
system('play -qV0 "|sox -np synth .07 sq 400" "|sox -np synth .5 sq 800" fade h 0 .5 .5 norm -20 &')
|
||||
elif mode == 'bypass':
|
||||
system('play -nqV0 synth .1 saw 700 saw 1200 delay 0 .04 remix - norm -6')
|
||||
elif mode == 'default':
|
||||
system('play -qV0 "|sox -np synth .07 sq 400" "|sox -np synth .5 sq 800" fade h 0 .5 .5 norm -20 reverse &')
|
||||
else:
|
||||
system('play -qV0 "|sox -np synth .07 sq 400" "|sox -np synth .5 sq 800" fade h 0 .5 .5 norm -20 &')
|
||||
system('play -nqV0 synth 0.05 pluck F3 norm -8 : synth 0.05 pluck C4 norm -8 : synth 0.05 pluck F4 norm -8 : synth 0.05 pluck C5 norm -8')
|
||||
|
||||
def on_workspace_focus(self,i3):
|
||||
#system('play -qnV0 synth pi fade 0 .25 .15 pad 0 1 reverb overdrive riaa norm -8 speed 1 &')
|
||||
@ -27,17 +47,18 @@ def on_workspace_move(self,i3):
|
||||
system('play -qnV0 synth pi fade 0 .25 .15 pad 0 1 reverb overdrive riaa norm -8 speed 1 reverse &')
|
||||
|
||||
def on_restart(self,i3):
|
||||
system('play -qn synth .25 saw 500:1200 fade .1 .25 .1 norm -8 &')
|
||||
system('play -qnV0 synth .25 saw 500:1200 fade .1 .25 .1 norm -8 &')
|
||||
|
||||
def on_exit(self,i3):
|
||||
system('play -qn synth .3 sin 700:200 fade 0 .3 0 &')
|
||||
system('play -qnV0 synth .3 sin 700:200 fade 0 .3 0 &')
|
||||
|
||||
def on_fullscreen(self,i3):
|
||||
system('play -qn synth br flanger fade h .3 .3 0 &')
|
||||
system('play -qnV0 synth br flanger fade h .3 .3 0 &')
|
||||
|
||||
i3 = i3ipc.Connection()
|
||||
|
||||
i3.on('window::new', on_new_window)
|
||||
i3.on('window::close', on_close_window)
|
||||
i3.on(Event.MODE, on_mode)
|
||||
i3.on('workspace::focus', on_workspace_focus)
|
||||
i3.on('window::move', on_workspace_move)
|
||||
|
110
scripts/sysinfo.sh
Executable file
110
scripts/sysinfo.sh
Executable file
@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
|
||||
# Initialize variables
|
||||
cpuUsage=0
|
||||
cpuTemp=0
|
||||
memoryUsed=0
|
||||
memoryTotal=0
|
||||
memoryPercent=0
|
||||
swapUsed=0
|
||||
swapTotal=0
|
||||
swapPercent=0
|
||||
diskUsed=0
|
||||
diskTotal=0
|
||||
diskPercent=0
|
||||
networkSent=0
|
||||
networkRecv=0
|
||||
|
||||
# Helper function for temperature conversion
|
||||
celsius_to_fahrenheit() {
|
||||
local celsius="$1"
|
||||
[[ -z "$celsius" || "$celsius" == "--" ]] && echo "--" && return
|
||||
[[ ! "$celsius" =~ ^-?[0-9]+(\.[0-9]+)?$ ]] && echo "--" && return
|
||||
|
||||
local fahrenheit
|
||||
fahrenheit=$(echo "scale=1; ($celsius * 9/5) + 32" | bc -l)
|
||||
echo "$fahrenheit"
|
||||
}
|
||||
|
||||
update_system_data() {
|
||||
# CPU usage
|
||||
cpuUsage=$(top -bn1 | grep "Cpu(s)" | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk '{print 100 - $1}')
|
||||
cpuUsage=$(printf "%.1f" "$cpuUsage" 2>/dev/null || echo "$cpuUsage")
|
||||
|
||||
# CPU temperature - fix for high readings
|
||||
local tempCelsius="--"
|
||||
if [[ -f /sys/class/thermal/thermal_zone0/temp ]]; then
|
||||
tempCelsius=$(cat /sys/class/thermal/thermal_zone0/temp)
|
||||
# Check if we need to divide by 1000 (common format)
|
||||
if [[ $tempCelsius -gt 200 ]]; then
|
||||
tempCelsius=$(echo "scale=1; $tempCelsius/1000" | bc -l)
|
||||
fi
|
||||
elif [[ -f /sys/class/hwmon/hwmon0/temp1_input ]]; then
|
||||
tempCelsius=$(cat /sys/class/hwmon/hwmon0/temp1_input)
|
||||
if [[ $tempCelsius -gt 200 ]]; then
|
||||
tempCelsius=$(echo "scale=1; $tempCelsius/1000" | bc -l)
|
||||
fi
|
||||
elif command -v sensors &>/dev/null; then
|
||||
tempCelsius=$(sensors | grep -oP 'Core 0.*?\+\K[0-9.]+')
|
||||
fi
|
||||
|
||||
[[ "$tempCelsius" != "--" && "$tempCelsius" != "null" ]] && cpuTemp=$(celsius_to_fahrenheit "$tempCelsius") || cpuTemp="--"
|
||||
|
||||
# Memory usage
|
||||
memoryTotal=$(free -m | awk '/^Mem:/{print $2/1024}')
|
||||
memoryUsed=$(free -m | awk '/^Mem:/{print $3/1024}')
|
||||
memoryPercent=$(free | awk '/^Mem:/{printf("%.1f", $3/$2 * 100)}')
|
||||
|
||||
# Swap usage
|
||||
swapTotal=$(free -m | awk '/^Swap:/{print $2/1024}')
|
||||
swapUsed=$(free -m | awk '/^Swap:/{print $3/1024}')
|
||||
[[ "$swapTotal" -gt 0 ]] && swapPercent=$(free | awk '/^Swap:/{printf("%.1f", $3/$2 * 100)}') || swapPercent=0
|
||||
|
||||
# Disk usage
|
||||
diskTotal=$(df -h / | awk 'NR==2 {print $2}')
|
||||
diskUsed=$(df -h / | awk 'NR==2 {print $3}')
|
||||
diskPercent=$(df -h / | awk 'NR==2 {print $5}' | tr -d '%')
|
||||
|
||||
# Network usage
|
||||
networkSent=$(cat /proc/net/dev | grep -v "lo:" | awk '{s+=$10} END {printf "%.2f", s/1024/1024}')
|
||||
networkRecv=$(cat /proc/net/dev | grep -v "lo:" | awk '{r+=$2} END {printf "%.2f", r/1024/1024}')
|
||||
}
|
||||
|
||||
display_system_info() {
|
||||
update_system_data
|
||||
|
||||
# Create the system information text with proper line breaks
|
||||
systemInfoText="System Information
|
||||
|
||||
CPU
|
||||
Usage: ${cpuUsage}%
|
||||
Temperature: ${cpuTemp}° F
|
||||
|
||||
Memory
|
||||
Usage: ${memoryUsed} / ${memoryTotal} GB (${memoryPercent}%)
|
||||
Swap: ${swapUsed} / ${swapTotal} GB (${swapPercent}%)
|
||||
|
||||
Disk (Root Partition)
|
||||
Usage: ${diskUsed} / ${diskTotal} GB (${diskPercent}%)
|
||||
|
||||
Network (Total Since Boot)
|
||||
Received: ${networkRecv} MB
|
||||
Sent: ${networkSent} MB
|
||||
|
||||
End of text. Press Control+Home to return to the beginning."
|
||||
|
||||
# Display in text-info dialog for screen reader accessibility
|
||||
echo "$systemInfoText" | yad --pname=I38System \
|
||||
--title="I38 System Information" \
|
||||
--text-info \
|
||||
--show-cursor \
|
||||
--width=400 \
|
||||
--height=400 \
|
||||
--center \
|
||||
--button="Close:0"
|
||||
}
|
||||
|
||||
display_system_info
|
||||
|
||||
exit 0
|
104
scripts/toggle_screenreader.sh
Executable file
104
scripts/toggle_screenreader.sh
Executable file
@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This file is part of I38.
|
||||
#
|
||||
# I38 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation,
|
||||
# either version 3 of the License, or (at your option) any later version.
|
||||
#
|
||||
# I38 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
# PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with I38. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
is_running() {
|
||||
pgrep -x "$1" >/dev/null
|
||||
return $?
|
||||
}
|
||||
|
||||
speak() {
|
||||
spd-say -P important -Cw -- "$*"
|
||||
}
|
||||
|
||||
# Make sure screen readers are available
|
||||
orcaAvailable=false
|
||||
cthulhuAvailable=false
|
||||
|
||||
if command -v orca &> /dev/null; then
|
||||
orcaAvailable=true
|
||||
fi
|
||||
|
||||
if command -v cthulhu &> /dev/null; then
|
||||
cthulhuAvailable=true
|
||||
fi
|
||||
|
||||
# Check if at least one screen reader is available
|
||||
if ! $orcaAvailable && ! $cthulhuAvailable; then
|
||||
speak "No screen readers found. Please install Orca or Cthulhu."
|
||||
yad --center --title="I38" --alert="No screen readers found.\nPlease install Orca or Cthulhu."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Determine current state
|
||||
currentReader="none"
|
||||
if is_running "cthulhu"; then
|
||||
currentReader="cthulhu"
|
||||
elif is_running "orca"; then
|
||||
currentReader="orca"
|
||||
fi
|
||||
|
||||
# Build YAD command based on available screen readers
|
||||
items=()
|
||||
|
||||
# First add the currently active screen reader
|
||||
if [ "$currentReader" != "none" ]; then
|
||||
if [ "$currentReader" = "cthulhu" ] && $cthulhuAvailable; then
|
||||
items+=("Cthulhu (active)" "cthulhu")
|
||||
elif [ "$currentReader" = "orca" ] && $orcaAvailable; then
|
||||
items+=("Orca (active)" "orca")
|
||||
fi
|
||||
fi
|
||||
|
||||
if $cthulhuAvailable && [ "$currentReader" != "cthulhu" ]; then
|
||||
items+=("Cthulhu" "cthulhu")
|
||||
fi
|
||||
|
||||
if $orcaAvailable && [ "$currentReader" != "orca" ]; then
|
||||
items+=("Orca" "orca")
|
||||
fi
|
||||
|
||||
# Display the dialog
|
||||
result=$(yad --center --title="Screen Reader Toggle" --text="Select screen reader:" \
|
||||
--list --on-top --skip-taskbar \
|
||||
--button="Cancel:1" --button="OK:0" \
|
||||
--column="Screen Reader" --column=ID:HD "${items[@]}")
|
||||
exitCode=$?
|
||||
if [ $exitCode -ne 0 ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -n "$result" ]; then
|
||||
# Extract the selected reader from the result
|
||||
selectedReader=$(echo "$result" | cut -d'|' -f2)
|
||||
|
||||
# Don't do anything if selecting the already active reader
|
||||
if [ "$selectedReader" = "$currentReader" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Stop current screen reader
|
||||
if [ "$currentReader" != "none" ]; then
|
||||
speak "Switching from $currentReader to $selectedReader."
|
||||
pkill -15 "$currentReader"
|
||||
sleep 0.5
|
||||
else
|
||||
speak "Starting $selectedReader."
|
||||
fi
|
||||
|
||||
if [ "$selectedReader" = "orca" ]; then
|
||||
orca &
|
||||
else
|
||||
cthulhu &
|
||||
fi
|
||||
fi
|
||||
|
||||
exit 0
|
439
scripts/weather.sh
Executable file
439
scripts/weather.sh
Executable file
@ -0,0 +1,439 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
|
||||
# Configuration settings
|
||||
defaultCity="Raleigh, NC"
|
||||
defaultLat="35.78"
|
||||
defaultLon="-78.64"
|
||||
tempUnit="F"
|
||||
updateInterval=30
|
||||
|
||||
configDir="${XDG_CONFIG_HOME:-$HOME/.config}/stormux/I38"
|
||||
configFile="$configDir/weather.conf"
|
||||
mkdir -p "$configDir"
|
||||
|
||||
# Initialize variables
|
||||
cityName="Detecting..."
|
||||
latitude=0.0
|
||||
longitude=0.0
|
||||
currentTemp="--"
|
||||
currentHumidity="--"
|
||||
currentWindSpeed="--"
|
||||
currentWindSpeedMph="--"
|
||||
currentConditions="Unknown"
|
||||
weatherLastUpdate=0
|
||||
severeWeatherAlerted=0
|
||||
|
||||
declare -a forecastDates=("--" "--" "--")
|
||||
declare -a forecastFormattedDates=("--" "--" "--")
|
||||
declare -a forecastMinTemps=("--" "--" "--")
|
||||
declare -a forecastMaxTemps=("--" "--" "--")
|
||||
declare -a forecastConditions=("Unknown" "Unknown" "Unknown")
|
||||
|
||||
declare -A weatherCodes
|
||||
weatherCodes[0]="Clear sky"
|
||||
weatherCodes[1]="Mainly clear"
|
||||
weatherCodes[2]="Partly cloudy"
|
||||
weatherCodes[3]="Overcast"
|
||||
weatherCodes[45]="Fog"
|
||||
weatherCodes[48]="Rime fog"
|
||||
weatherCodes[51]="Light drizzle"
|
||||
weatherCodes[53]="Moderate drizzle"
|
||||
weatherCodes[55]="Dense drizzle"
|
||||
weatherCodes[56]="Light freezing drizzle"
|
||||
weatherCodes[57]="Dense freezing drizzle"
|
||||
weatherCodes[61]="Slight rain"
|
||||
weatherCodes[63]="Moderate rain"
|
||||
weatherCodes[65]="Heavy rain"
|
||||
weatherCodes[66]="Light freezing rain"
|
||||
weatherCodes[67]="Heavy freezing rain"
|
||||
weatherCodes[71]="Slight snow fall"
|
||||
weatherCodes[73]="Moderate snow fall"
|
||||
weatherCodes[75]="Heavy snow fall"
|
||||
weatherCodes[77]="Snow flurries"
|
||||
weatherCodes[80]="Slight rain showers"
|
||||
weatherCodes[81]="Moderate rain showers"
|
||||
weatherCodes[82]="Heavy rain showers"
|
||||
weatherCodes[85]="Slight snow showers"
|
||||
weatherCodes[86]="Heavy snow showers"
|
||||
weatherCodes[95]="Thunderstorm"
|
||||
weatherCodes[96]="Thunderstorm with slight hail"
|
||||
weatherCodes[99]="Thunderstorm with heavy hail"
|
||||
|
||||
declare -a severeWeatherCodes=(65 67 75 82 86 95 96 99)
|
||||
|
||||
# Button return codes
|
||||
refreshBtn=0
|
||||
quitBtn=1
|
||||
settingsBtn=2
|
||||
|
||||
trap "pkill -P $$" EXIT INT TERM
|
||||
|
||||
# Load configuration if available
|
||||
if [ -f "$configFile" ]; then
|
||||
source "$configFile"
|
||||
# Convert lastWeatherUpdate string to integer if it exists
|
||||
[[ -n "$lastWeatherUpdate" ]] && weatherLastUpdate=$lastWeatherUpdate || weatherLastUpdate=0
|
||||
|
||||
if [[ -n "$city" ]]; then
|
||||
cityName="$city"
|
||||
latitude="$latitude"
|
||||
longitude="$longitude"
|
||||
fi
|
||||
|
||||
# Try to reload saved weather data
|
||||
if [[ "$weatherLastUpdate" -gt 0 && "$currentTemp" == "--" ]]; then
|
||||
[[ -n "$savedCurrentTemp" ]] && currentTemp="$savedCurrentTemp"
|
||||
[[ -n "$savedCurrentHumidity" ]] && currentHumidity="$savedCurrentHumidity"
|
||||
[[ -n "$savedCurrentConditions" ]] && currentConditions="$savedCurrentConditions"
|
||||
[[ -n "$savedCurrentWindSpeed" ]] && currentWindSpeedMph="$savedCurrentWindSpeed"
|
||||
|
||||
for i in {0..2}; do
|
||||
varDate="savedForecastDate_$i"
|
||||
varMin="savedForecastMin_$i"
|
||||
varMax="savedForecastMax_$i"
|
||||
varCond="savedForecastCond_$i"
|
||||
|
||||
[[ -n "${!varDate}" ]] && forecastFormattedDates[$i]="${!varDate}"
|
||||
[[ -n "${!varMin}" ]] && forecastMinTemps[$i]="${!varMin}"
|
||||
[[ -n "${!varMax}" ]] && forecastMaxTemps[$i]="${!varMax}"
|
||||
[[ -n "${!varCond}" ]] && forecastConditions[$i]="${!varCond}"
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
# Helper functions
|
||||
time_diff() {
|
||||
local timestamp="$1"
|
||||
local now=$(date +%s)
|
||||
local diff=$((now - timestamp))
|
||||
|
||||
if [ $diff -lt 60 ]; then
|
||||
echo "just now"
|
||||
elif [ $diff -lt 3600 ]; then
|
||||
local minutes=$((diff / 60))
|
||||
echo "$minutes minute$([ $minutes -ne 1 ] && echo "s") ago"
|
||||
elif [ $diff -lt 86400 ]; then
|
||||
local hours=$((diff / 3600))
|
||||
echo "$hours hour$([ $hours -ne 1 ] && echo "s") ago"
|
||||
else
|
||||
local days=$((diff / 86400))
|
||||
echo "$days day$([ $days -ne 1 ] && echo "s") ago"
|
||||
fi
|
||||
}
|
||||
|
||||
celsius_to_fahrenheit() {
|
||||
local celsius="$1"
|
||||
[[ -z "$celsius" || "$celsius" == "--" ]] && echo "--" && return
|
||||
[[ ! "$celsius" =~ ^-?[0-9]+(\.[0-9]+)?$ ]] && echo "--" && return
|
||||
|
||||
local fahrenheit
|
||||
fahrenheit=$(echo "scale=1; ($celsius * 9/5) + 32" | bc -l)
|
||||
echo "$fahrenheit"
|
||||
}
|
||||
|
||||
kmh_to_mph() {
|
||||
local kmh="$1"
|
||||
[[ -z "$kmh" || "$kmh" == "--" ]] && echo "--" && return
|
||||
[[ ! "$kmh" =~ ^-?[0-9]+(\.[0-9]+)?$ ]] && echo "--" && return
|
||||
|
||||
local mph
|
||||
mph=$(echo "scale=1; $kmh * 0.621371" | bc -l)
|
||||
echo "$mph"
|
||||
}
|
||||
|
||||
format_date() {
|
||||
local isoDate="$1"
|
||||
[[ -z "$isoDate" || "$isoDate" == "--" ]] && echo "--" && return
|
||||
date -d "$isoDate" "+%A, %B %d" 2>/dev/null || echo "$isoDate"
|
||||
}
|
||||
|
||||
# Save configuration
|
||||
save_config() {
|
||||
cat > "$configFile" << EOF
|
||||
city="$cityName"
|
||||
latitude="$latitude"
|
||||
longitude="$longitude"
|
||||
tempUnit=$tempUnit
|
||||
updateInterval=$updateInterval
|
||||
lastWeatherUpdate=$weatherLastUpdate
|
||||
savedCurrentTemp="$currentTemp"
|
||||
savedCurrentHumidity="$currentHumidity"
|
||||
savedCurrentConditions="$currentConditions"
|
||||
savedCurrentWindSpeed="$currentWindSpeedMph"
|
||||
savedForecastDate_0="${forecastFormattedDates[0]}"
|
||||
savedForecastMin_0="${forecastMinTemps[0]}"
|
||||
savedForecastMax_0="${forecastMaxTemps[0]}"
|
||||
savedForecastCond_0="${forecastConditions[0]}"
|
||||
savedForecastDate_1="${forecastFormattedDates[1]}"
|
||||
savedForecastMin_1="${forecastMinTemps[1]}"
|
||||
savedForecastMax_1="${forecastMaxTemps[1]}"
|
||||
savedForecastCond_1="${forecastConditions[1]}"
|
||||
savedForecastDate_2="${forecastFormattedDates[2]}"
|
||||
savedForecastMin_2="${forecastMinTemps[2]}"
|
||||
savedForecastMax_2="${forecastMaxTemps[2]}"
|
||||
savedForecastCond_2="${forecastConditions[2]}"
|
||||
EOF
|
||||
}
|
||||
|
||||
# Play severe weather alert sound using Sox
|
||||
play_severe_weather_alert() {
|
||||
if command -v play &>/dev/null; then
|
||||
# Generate alert sound pattern using sox
|
||||
play -nqV0 synth 2 sine 853 sine 960 remix - norm -15 &
|
||||
fi
|
||||
|
||||
# Also display notification if available
|
||||
if command -v notify-send &>/dev/null; then
|
||||
notify-send "Severe Weather Alert" "Severe weather conditions detected for $cityName: $currentConditions" -u critical
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to check if a value is in array
|
||||
in_array() {
|
||||
local value="$1"
|
||||
shift
|
||||
local array=("$@")
|
||||
|
||||
for item in "${array[@]}"; do
|
||||
if [[ "$item" == "$value" ]]; then
|
||||
return 0 # True, found in array
|
||||
fi
|
||||
done
|
||||
return 1 # False, not found
|
||||
}
|
||||
|
||||
# Function to detect location
|
||||
get_location() {
|
||||
# Only try location detection if we don't already have a city name
|
||||
if [[ "$cityName" == "Detecting..." ]]; then
|
||||
echo "Attempting to detect location via ipinfo.io..."
|
||||
|
||||
# Try to fetch location data
|
||||
local locationData
|
||||
locationData=$(curl -s --connect-timeout 5 "https://ipinfo.io/json" 2>/dev/null)
|
||||
|
||||
if [[ $? -eq 0 && -n "$locationData" && $(echo "$locationData" | jq -e '.city') ]]; then
|
||||
echo "Location data received successfully"
|
||||
cityName=$(echo "$locationData" | jq -r '.city // "Unknown"')
|
||||
local region=$(echo "$locationData" | jq -r '.region // ""')
|
||||
# Add region/state to city name if available
|
||||
[[ -n "$region" ]] && cityName="$cityName, $region"
|
||||
|
||||
# Extract coordinates directly from the "loc" field
|
||||
local loc=$(echo "$locationData" | jq -r '.loc // "0,0"')
|
||||
latitude=$(echo "$loc" | cut -d',' -f1)
|
||||
longitude=$(echo "$loc" | cut -d',' -f2)
|
||||
save_config
|
||||
else
|
||||
cityName="$defaultCity"
|
||||
latitude="$defaultLat"
|
||||
longitude="$defaultLon"
|
||||
save_config
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to fetch weather data
|
||||
fetch_weather_data() {
|
||||
local now=$(date +%s)
|
||||
local elapsedMinutes=$(( (now - weatherLastUpdate) / 60 ))
|
||||
|
||||
# Only fetch if needed
|
||||
if [[ $weatherLastUpdate -eq 0 || $elapsedMinutes -ge $updateInterval ]]; then
|
||||
local url="https://api.open-meteo.com/v1/forecast?latitude=$latitude&longitude=$longitude¤t=temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m&daily=weather_code,temperature_2m_max,temperature_2m_min&timezone=auto"
|
||||
local response=$(curl -s --connect-timeout 10 "$url" 2>/dev/null)
|
||||
|
||||
if [[ $? -eq 0 && -n "$response" && $(echo "$response" | jq -e '.current' 2>/dev/null) ]]; then
|
||||
# Update current weather data
|
||||
local tempCelsius=$(echo "$response" | jq -r '.current.temperature_2m // "--"' 2>/dev/null)
|
||||
[[ "$tempCelsius" != "--" && "$tempCelsius" != "null" ]] && currentTemp=$(celsius_to_fahrenheit "$tempCelsius") || currentTemp="--"
|
||||
|
||||
currentHumidity=$(echo "$response" | jq -r '.current.relative_humidity_2m // "--"' 2>/dev/null)
|
||||
[[ "$currentHumidity" == "null" ]] && currentHumidity="--"
|
||||
|
||||
currentWindSpeed=$(echo "$response" | jq -r '.current.wind_speed_10m // "--"' 2>/dev/null)
|
||||
if [[ "$currentWindSpeed" != "--" && "$currentWindSpeed" != "null" ]]; then
|
||||
currentWindSpeedMph=$(kmh_to_mph "$currentWindSpeed")
|
||||
else
|
||||
currentWindSpeed="--"
|
||||
currentWindSpeedMph="--"
|
||||
fi
|
||||
|
||||
local weatherCode=$(echo "$response" | jq -r '.current.weather_code // 0' 2>/dev/null)
|
||||
[[ "$weatherCode" == "null" ]] && weatherCode=0
|
||||
currentConditions="${weatherCodes[$weatherCode]:-Unknown}"
|
||||
|
||||
# Check for severe weather and play alert if needed
|
||||
if in_array "$weatherCode" "${severeWeatherCodes[@]}"; then
|
||||
if [ "$severeWeatherAlerted" -eq 0 ]; then
|
||||
play_severe_weather_alert
|
||||
severeWeatherAlerted=1
|
||||
fi
|
||||
else
|
||||
# Reset alert flag if weather is no longer severe
|
||||
severeWeatherAlerted=0
|
||||
fi
|
||||
|
||||
# Process forecast data (limited to 3 days)
|
||||
if [[ $(echo "$response" | jq -e '.daily' 2>/dev/null) ]]; then
|
||||
for i in {0..2}; do
|
||||
# Process forecast data
|
||||
forecastDates[$i]=$(echo "$response" | jq -r ".daily.time[$i] // \"--\"" 2>/dev/null)
|
||||
[[ "${forecastDates[$i]}" != "--" && "${forecastDates[$i]}" != "null" ]] && \
|
||||
forecastFormattedDates[$i]=$(format_date "${forecastDates[$i]}") || forecastFormattedDates[$i]="--"
|
||||
|
||||
local minTempC=$(echo "$response" | jq -r ".daily.temperature_2m_min[$i] // \"--\"" 2>/dev/null)
|
||||
[[ "$minTempC" != "--" && "$minTempC" != "null" ]] && \
|
||||
forecastMinTemps[$i]=$(celsius_to_fahrenheit "$minTempC") || forecastMinTemps[$i]="--"
|
||||
|
||||
local maxTempC=$(echo "$response" | jq -r ".daily.temperature_2m_max[$i] // \"--\"" 2>/dev/null)
|
||||
[[ "$maxTempC" != "--" && "$maxTempC" != "null" ]] && \
|
||||
forecastMaxTemps[$i]=$(celsius_to_fahrenheit "$maxTempC") || forecastMaxTemps[$i]="--"
|
||||
|
||||
local code=$(echo "$response" | jq -r ".daily.weather_code[$i] // 0" 2>/dev/null)
|
||||
[[ "$code" == "null" ]] && code=0
|
||||
forecastConditions[$i]="${weatherCodes[$code]:-Unknown}"
|
||||
done
|
||||
fi
|
||||
|
||||
# Update timestamp
|
||||
weatherLastUpdate=$(date +%s)
|
||||
save_config
|
||||
else
|
||||
echo "Failed to fetch weather data. Response code: $?"
|
||||
if [[ -n "$response" ]]; then
|
||||
echo "First 100 chars of response: ${response:0:100}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to change location (for settings)
|
||||
change_location() {
|
||||
local newLocation="$1"
|
||||
|
||||
if [[ -n "$newLocation" && "$newLocation" != "$cityName" ]]; then
|
||||
# Try to parse the location using curl to a geocoding service
|
||||
local result=$(curl -s --connect-timeout 10 "https://nominatim.openstreetmap.org/search?q=$newLocation&format=json" 2>/dev/null)
|
||||
|
||||
if [[ -n "$result" && $(echo "$result" | jq -e '.[0]') ]]; then
|
||||
cityName="$newLocation"
|
||||
latitude=$(echo "$result" | jq -r '.[0].lat // "0.0"')
|
||||
longitude=$(echo "$result" | jq -r '.[0].lon // "0.0"')
|
||||
|
||||
# Force weather update
|
||||
weatherLastUpdate=0
|
||||
save_config
|
||||
return 0
|
||||
else
|
||||
yad --title "Location Error" --text="Could not find location: $newLocation" --button=gtk-ok
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# Display weather information in a text-info dialog
|
||||
display_weather() {
|
||||
local lastUpdateText="Never updated"
|
||||
[[ "$weatherLastUpdate" -gt 0 ]] && lastUpdateText="Last updated: $(time_diff "$weatherLastUpdate")"
|
||||
|
||||
# Create the weather information text with proper line breaks
|
||||
weatherInfoText="Weather for $cityName
|
||||
$lastUpdateText
|
||||
|
||||
Current Conditions
|
||||
Temperature: ${currentTemp}° F
|
||||
Conditions: $currentConditions
|
||||
Humidity: ${currentHumidity}%
|
||||
Wind Speed: ${currentWindSpeedMph} mph
|
||||
|
||||
3-Day Forecast
|
||||
────────────────────────────────────
|
||||
${forecastFormattedDates[0]}
|
||||
Temp: ${forecastMinTemps[0]}° to ${forecastMaxTemps[0]}° F
|
||||
Conditions: ${forecastConditions[0]}
|
||||
────────────────────────────────────
|
||||
${forecastFormattedDates[1]}
|
||||
Temp: ${forecastMinTemps[1]}° to ${forecastMaxTemps[1]}° F
|
||||
Conditions: ${forecastConditions[1]}
|
||||
────────────────────────────────────
|
||||
${forecastFormattedDates[2]}
|
||||
Temp: ${forecastMinTemps[2]}° to ${forecastMaxTemps[2]}° F
|
||||
Conditions: ${forecastConditions[2]}
|
||||
|
||||
End of text. Press Control+Home to return to the beginning."
|
||||
|
||||
# Display in text-info dialog for screen reader accessibility
|
||||
echo "$weatherInfoText" | yad --pname=I38Weather \
|
||||
--title="I38 Weather Monitor" \
|
||||
--text-info \
|
||||
--show-cursor \
|
||||
--width=500 \
|
||||
--height=600 \
|
||||
--center \
|
||||
--button="Settings:$settingsBtn" \
|
||||
--button="Refresh:$refreshBtn" \
|
||||
--button="Close:$quitBtn"
|
||||
|
||||
return $?
|
||||
}
|
||||
|
||||
# Function to display settings dialog
|
||||
display_settings() {
|
||||
local ret=$(yad --pname=I38WeatherSettings \
|
||||
--title="I38 Weather Settings" \
|
||||
--form \
|
||||
--width=400 \
|
||||
--center \
|
||||
--field="Location:":TEXT "$cityName" \
|
||||
--field="Current Coordinates:":LBL "Lat: $latitude, Lon: $longitude" \
|
||||
--field="Temperature Unit:":CB "F!C" \
|
||||
--field="Update Interval (minutes):":NUM "$updateInterval!5..120!5" \
|
||||
--button="Cancel:1" \
|
||||
--button="Save:0")
|
||||
|
||||
local saveResult=$?
|
||||
|
||||
if [[ $saveResult -eq 0 && -n "$ret" ]]; then
|
||||
local newLocation=$(echo "$ret" | cut -d"|" -f1)
|
||||
local newUnit=$(echo "$ret" | cut -d"|" -f3)
|
||||
local newInterval=$(echo "$ret" | cut -d"|" -f4)
|
||||
|
||||
# Apply any changes
|
||||
[[ -n "$newLocation" && "$newLocation" != "$cityName" ]] && change_location "$newLocation"
|
||||
[[ -n "$newUnit" && "$newUnit" != "$tempUnit" ]] && tempUnit="$newUnit" && save_config
|
||||
[[ -n "$newInterval" && "$newInterval" != "$updateInterval" ]] && updateInterval="$newInterval" && save_config
|
||||
fi
|
||||
}
|
||||
|
||||
# Main loop
|
||||
while : ; do
|
||||
get_location
|
||||
fetch_weather_data
|
||||
|
||||
# Display weather using the text-info widget
|
||||
display_weather
|
||||
ret=$?
|
||||
|
||||
# Handle button actions
|
||||
case $ret in
|
||||
$refreshBtn)
|
||||
# Force a weather update
|
||||
weatherLastUpdate=0
|
||||
continue
|
||||
;;
|
||||
$settingsBtn)
|
||||
# Display settings dialog
|
||||
display_settings
|
||||
continue
|
||||
;;
|
||||
$quitBtn|252)
|
||||
# Quit button or window closed
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
exit 0
|
@ -1,5 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This file is part of I38.
|
||||
|
||||
# I38 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation,
|
||||
# either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
# I38 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
# PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with I38. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
path="$(readlink -f $0)"
|
||||
path="${path%/*/*}"
|
||||
path="${path##*/}"
|
||||
if [[ "$path" == "i3" ]]; then
|
||||
mapfile -t windowList < <(python3 -c '
|
||||
import i3ipc
|
||||
|
||||
@ -13,4 +28,28 @@ id="$(yad --title "I38" --list --separator "" --column "id" --column "Select Win
|
||||
if [[ -z "${id}" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
i3-msg \[id="${id}"\] focus
|
||||
i3-msg \[id="${id}"\] focus
|
||||
else
|
||||
mapfile -t windowList < <(python3 -c '
|
||||
import i3ipc
|
||||
|
||||
i3 = i3ipc.Connection()
|
||||
|
||||
for con in i3.get_tree():
|
||||
if con.window or con.type == "con":
|
||||
if con.name:
|
||||
print(con.window)
|
||||
print(con.name)')
|
||||
|
||||
# Remove the first entry if it is "none"
|
||||
if [[ "${windowList[0]}" == "none" ]]; then
|
||||
unset "windowList[0]"
|
||||
fi
|
||||
|
||||
id="$(yad --title "I38" --list --separator "" --column "id" --column "Select Window" --hide-column 1 --print-column 1 "${windowList[@]}")"
|
||||
|
||||
if [[ -z "${id}" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
swaymsg \[id="${id}"\] focus
|
||||
fi
|
||||
|
444
scripts/wormhole.py
Executable file
444
scripts/wormhole.py
Executable file
@ -0,0 +1,444 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# This file is part of I38.
|
||||
|
||||
# I38 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation,
|
||||
# either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
# I38 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
# PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License along with I38. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import gi
|
||||
import os
|
||||
import subprocess
|
||||
import threading
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gdk, GLib, Pango
|
||||
|
||||
DEFAULT_DOWNLOAD_DIR = os.path.expanduser("~/Downloads")
|
||||
|
||||
class WormholeGUI(Gtk.Window):
|
||||
def __init__(self):
|
||||
super().__init__(title="Magic Wormhole GUI")
|
||||
self.set_border_width(10)
|
||||
self.set_default_size(500, 400)
|
||||
|
||||
self.download_dir = DEFAULT_DOWNLOAD_DIR
|
||||
self.notebook = Gtk.Notebook()
|
||||
self.add(self.notebook)
|
||||
|
||||
self.init_main_tab()
|
||||
self.init_settings_tab()
|
||||
|
||||
# Escape key closes app
|
||||
self.connect("key-press-event", self.on_key_press)
|
||||
|
||||
def init_main_tab(self):
|
||||
main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
|
||||
self.notebook.append_page(main_box, Gtk.Label(label="Main"))
|
||||
|
||||
button_box = Gtk.Box(spacing=10)
|
||||
self.send_button = Gtk.Button(label="Send")
|
||||
self.send_button.connect("clicked", self.on_send_clicked)
|
||||
button_box.pack_start(self.send_button, True, True, 0)
|
||||
|
||||
self.receive_button = Gtk.Button(label="Receive")
|
||||
self.receive_button.connect("clicked", self.on_receive_clicked)
|
||||
button_box.pack_start(self.receive_button, True, True, 0)
|
||||
|
||||
main_box.pack_start(button_box, False, False, 0)
|
||||
|
||||
# Add a frame for the code display
|
||||
code_frame = Gtk.Frame(label="Wormhole Code")
|
||||
main_box.pack_start(code_frame, False, False, 5)
|
||||
|
||||
code_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
|
||||
code_box.set_border_width(5)
|
||||
code_frame.add(code_box)
|
||||
|
||||
self.code_display = Gtk.Entry()
|
||||
self.code_display.set_editable(False)
|
||||
code_box.pack_start(self.code_display, False, False, 0)
|
||||
|
||||
# Add a frame for progress output
|
||||
progress_frame = Gtk.Frame(label="Transfer Progress")
|
||||
main_box.pack_start(progress_frame, True, True, 5)
|
||||
|
||||
# Add a scrolled window for the progress text
|
||||
scrolled_window = Gtk.ScrolledWindow()
|
||||
scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
||||
progress_frame.add(scrolled_window)
|
||||
|
||||
# Add a text view for progress output
|
||||
self.progress_text = Gtk.TextView()
|
||||
self.progress_text.set_editable(False)
|
||||
self.progress_text.set_cursor_visible(False)
|
||||
self.progress_text.set_wrap_mode(Gtk.WrapMode.WORD)
|
||||
self.progress_text.override_font(Pango.FontDescription("Monospace 10"))
|
||||
self.progress_buffer = self.progress_text.get_buffer()
|
||||
scrolled_window.add(self.progress_text)
|
||||
|
||||
# Add action buttons
|
||||
action_box = Gtk.Box(spacing=10)
|
||||
self.copy_button = Gtk.Button(label="Copy Code")
|
||||
self.copy_button.connect("clicked", self.copy_code)
|
||||
action_box.pack_start(self.copy_button, True, True, 0)
|
||||
|
||||
self.cancel_button = Gtk.Button(label="Cancel")
|
||||
self.cancel_button.connect("clicked", self.cancel_transfer)
|
||||
action_box.pack_start(self.cancel_button, True, True, 0)
|
||||
|
||||
main_box.pack_start(action_box, False, False, 0)
|
||||
|
||||
def init_settings_tab(self):
|
||||
settings_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
|
||||
settings_box.set_border_width(10)
|
||||
self.notebook.append_page(settings_box, Gtk.Label(label="Settings"))
|
||||
|
||||
# Create a frame for download settings
|
||||
download_frame = Gtk.Frame(label="Download Location")
|
||||
settings_box.pack_start(download_frame, False, False, 0)
|
||||
|
||||
# Add a container for the frame content
|
||||
download_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10)
|
||||
download_box.set_border_width(10)
|
||||
download_frame.add(download_box)
|
||||
|
||||
# Add a description label
|
||||
description = Gtk.Label(label="Files will be saved to this directory:")
|
||||
description.set_xalign(0) # Align to left
|
||||
download_box.pack_start(description, False, False, 0)
|
||||
|
||||
# Add a directory selector
|
||||
dir_box = Gtk.Box(spacing=5)
|
||||
download_box.pack_start(dir_box, False, False, 5)
|
||||
|
||||
# Add an entry to show the current path
|
||||
self.dir_entry = Gtk.Entry()
|
||||
self.dir_entry.set_text(self.download_dir)
|
||||
dir_box.pack_start(self.dir_entry, True, True, 0)
|
||||
|
||||
# Add a browse button
|
||||
browse_button = Gtk.Button(label="Browse...")
|
||||
browse_button.connect("clicked", self.on_browse_clicked)
|
||||
dir_box.pack_start(browse_button, False, False, 0)
|
||||
|
||||
# Add a save button
|
||||
save_button = Gtk.Button(label="Save Settings")
|
||||
save_button.connect("clicked", self.on_save_settings)
|
||||
download_box.pack_start(save_button, False, False, 5)
|
||||
|
||||
def on_key_press(self, widget, event):
|
||||
if event.keyval == Gdk.KEY_Escape:
|
||||
# Check if a transfer is currently in progress
|
||||
if hasattr(self, 'current_process') and self.current_process and self.current_process.poll() is None:
|
||||
# Show a dialog indicating transfer is in progress
|
||||
dialog = Gtk.MessageDialog(
|
||||
parent=self,
|
||||
flags=0,
|
||||
message_type=Gtk.MessageType.WARNING,
|
||||
buttons=Gtk.ButtonsType.OK,
|
||||
text="Transfer in Progress"
|
||||
)
|
||||
dialog.format_secondary_text("Please wait for the transfer to complete or cancel it before closing.")
|
||||
dialog.run()
|
||||
dialog.destroy()
|
||||
return True
|
||||
else:
|
||||
# No transfer in progress, confirm quit
|
||||
dialog = Gtk.MessageDialog(
|
||||
parent=self,
|
||||
flags=0,
|
||||
message_type=Gtk.MessageType.QUESTION,
|
||||
buttons=Gtk.ButtonsType.YES_NO,
|
||||
text="Quit Application"
|
||||
)
|
||||
dialog.format_secondary_text("Are you sure you want to quit?")
|
||||
response = dialog.run()
|
||||
dialog.destroy()
|
||||
|
||||
if response == Gtk.ResponseType.YES:
|
||||
Gtk.main_quit()
|
||||
|
||||
return True
|
||||
return False
|
||||
|
||||
def on_browse_clicked(self, button):
|
||||
"""Browse for a directory"""
|
||||
dialog = Gtk.FileChooserDialog(
|
||||
title="Select Download Directory",
|
||||
parent=self,
|
||||
action=Gtk.FileChooserAction.SELECT_FOLDER,
|
||||
buttons=(
|
||||
Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
|
||||
Gtk.STOCK_OPEN, Gtk.ResponseType.OK
|
||||
)
|
||||
)
|
||||
|
||||
# Set the current folder to the current download directory
|
||||
if os.path.exists(self.download_dir):
|
||||
dialog.set_current_folder(self.download_dir)
|
||||
|
||||
if dialog.run() == Gtk.ResponseType.OK:
|
||||
self.dir_entry.set_text(dialog.get_filename())
|
||||
|
||||
dialog.destroy()
|
||||
|
||||
def on_save_settings(self, button):
|
||||
"""Save the settings"""
|
||||
new_dir = self.dir_entry.get_text().strip()
|
||||
|
||||
# Handle ~ in path
|
||||
if new_dir.startswith("~"):
|
||||
new_dir = os.path.expanduser(new_dir)
|
||||
|
||||
# Validate the directory
|
||||
if not os.path.isdir(new_dir):
|
||||
try:
|
||||
os.makedirs(new_dir, exist_ok=True)
|
||||
except Exception as e:
|
||||
self.show_error(f"Could not create directory: {e}")
|
||||
return
|
||||
|
||||
# Update the directory
|
||||
self.download_dir = new_dir
|
||||
|
||||
# Show confirmation
|
||||
self.show_info("Settings saved successfully.")
|
||||
|
||||
def on_download_dir_changed(self, widget):
|
||||
self.download_dir = widget.get_filename()
|
||||
|
||||
def on_send_clicked(self, widget):
|
||||
chooser = Gtk.FileChooserDialog(
|
||||
title="Select File or Folder", parent=self,
|
||||
action=Gtk.FileChooserAction.OPEN,
|
||||
buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
|
||||
Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
|
||||
)
|
||||
chooser.set_select_multiple(False)
|
||||
chooser.set_local_only(True)
|
||||
chooser.set_modal(True)
|
||||
chooser.set_property("show-hidden", False)
|
||||
chooser.connect("key-press-event", self.on_key_press)
|
||||
|
||||
if chooser.run() == Gtk.ResponseType.OK:
|
||||
path = chooser.get_filename()
|
||||
chooser.destroy()
|
||||
self.send_file(path)
|
||||
else:
|
||||
chooser.destroy()
|
||||
|
||||
def send_file(self, path):
|
||||
self.code_display.set_text("Sending...")
|
||||
self.clear_progress()
|
||||
self.update_progress(f"Starting to send: {os.path.basename(path)}\n")
|
||||
|
||||
# Initialize current_process attribute
|
||||
self.current_process = None
|
||||
|
||||
def send():
|
||||
self.current_process = subprocess.Popen(
|
||||
["wormhole", "send", path],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
stdin=subprocess.DEVNULL,
|
||||
text=True
|
||||
)
|
||||
for line in self.current_process.stdout:
|
||||
print("SEND OUTPUT:", line.strip()) # Debug info
|
||||
|
||||
# Update the progress display
|
||||
GLib.idle_add(self.update_progress, line)
|
||||
|
||||
if "Wormhole code is:" in line:
|
||||
code = line.strip().split(":", 1)[-1].strip()
|
||||
GLib.idle_add(self.code_display.set_text, code)
|
||||
self.current_process.stdout.close()
|
||||
self.current_process.wait()
|
||||
|
||||
# Clear the current_process when done
|
||||
GLib.idle_add(self.clear_current_process)
|
||||
|
||||
threading.Thread(target=send, daemon=True).start()
|
||||
|
||||
def on_receive_clicked(self, widget):
|
||||
dialog = Gtk.Dialog(
|
||||
title="Enter Wormhole Code",
|
||||
parent=self,
|
||||
flags=0
|
||||
)
|
||||
dialog.add_buttons(
|
||||
Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
|
||||
Gtk.STOCK_OK, Gtk.ResponseType.OK
|
||||
)
|
||||
dialog.connect("key-press-event", self.on_key_press)
|
||||
|
||||
# Make OK button the default
|
||||
ok_button = dialog.get_widget_for_response(Gtk.ResponseType.OK)
|
||||
ok_button.set_can_default(True)
|
||||
ok_button.grab_default()
|
||||
|
||||
entry = Gtk.Entry()
|
||||
entry.set_activates_default(True)
|
||||
entry.grab_focus()
|
||||
|
||||
box = dialog.get_content_area()
|
||||
box.set_border_width(10)
|
||||
box.set_spacing(10)
|
||||
box.add(Gtk.Label(label="Enter the wormhole code:"))
|
||||
box.add(entry)
|
||||
box.show_all()
|
||||
|
||||
response = dialog.run()
|
||||
if response == Gtk.ResponseType.OK:
|
||||
code = entry.get_text()
|
||||
dialog.destroy()
|
||||
self.receive_file(code)
|
||||
else:
|
||||
dialog.destroy()
|
||||
|
||||
def receive_file(self, code):
|
||||
self.code_display.set_text("Receiving...")
|
||||
self.clear_progress()
|
||||
self.update_progress(f"Starting to receive with code: {code}\n")
|
||||
|
||||
# Initialize current_process attribute
|
||||
self.current_process = None
|
||||
|
||||
def receive():
|
||||
# Save current directory
|
||||
original_dir = os.getcwd()
|
||||
|
||||
try:
|
||||
# Create download directory if it doesn't exist
|
||||
os.makedirs(self.download_dir, exist_ok=True)
|
||||
|
||||
# Change to download directory before starting the process
|
||||
os.chdir(self.download_dir)
|
||||
|
||||
self.update_progress(f"Downloading to: {self.download_dir}\n")
|
||||
|
||||
# Start the wormhole receive process
|
||||
self.current_process = subprocess.Popen(
|
||||
["wormhole", "receive", "--accept-file"],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True
|
||||
)
|
||||
|
||||
# Send the code to the process
|
||||
self.current_process.stdin.write(code + "\n")
|
||||
self.current_process.stdin.flush()
|
||||
self.current_process.stdin.close()
|
||||
|
||||
for line in self.current_process.stdout:
|
||||
print("RECEIVE OUTPUT:", line.strip()) # Debug info
|
||||
|
||||
# Update the progress display
|
||||
GLib.idle_add(self.update_progress, line)
|
||||
|
||||
# Handle questions about accepting the file
|
||||
if "ok? (y/N):" in line:
|
||||
# Auto-accept the file
|
||||
self.current_process.stdin = open("/dev/stdin", "w")
|
||||
self.current_process.stdin.write("y\n")
|
||||
self.current_process.stdin.flush()
|
||||
self.current_process.stdin.close()
|
||||
|
||||
if "Received file" in line or "File received" in line:
|
||||
GLib.idle_add(self.code_display.set_text, "File received.")
|
||||
|
||||
self.current_process.stdout.close()
|
||||
self.current_process.wait()
|
||||
|
||||
# Add final status message
|
||||
GLib.idle_add(self.update_progress, f"\nFile saved to: {self.download_dir}\n")
|
||||
|
||||
except Exception as e:
|
||||
GLib.idle_add(self.update_progress, f"Error: {e}\n")
|
||||
|
||||
finally:
|
||||
# Change back to original directory
|
||||
os.chdir(original_dir)
|
||||
|
||||
# Clear the current_process when done
|
||||
GLib.idle_add(self.clear_current_process)
|
||||
|
||||
threading.Thread(target=receive, daemon=True).start()
|
||||
|
||||
def copy_code(self, widget):
|
||||
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
|
||||
clipboard.set_text(self.code_display.get_text(), -1)
|
||||
self.update_progress("Code copied to clipboard.\n")
|
||||
|
||||
def cancel_transfer(self, widget):
|
||||
if hasattr(self, 'current_process') and self.current_process and self.current_process.poll() is None:
|
||||
try:
|
||||
self.current_process.terminate()
|
||||
self.update_progress("Transfer process terminated.\n")
|
||||
except Exception as e:
|
||||
self.update_progress(f"Error canceling transfer: {e}\n")
|
||||
|
||||
self.code_display.set_text("Transfer canceled.")
|
||||
self.update_progress("Transfer canceled by user.\n")
|
||||
self.clear_current_process()
|
||||
|
||||
def update_progress(self, text):
|
||||
"""Update the progress text view"""
|
||||
end = self.progress_buffer.get_end_iter()
|
||||
self.progress_buffer.insert(end, text)
|
||||
|
||||
# Scroll to the end
|
||||
self.progress_text.scroll_to_iter(end, 0.0, False, 0.0, 0.0)
|
||||
|
||||
return False # Required for GLib.idle_add
|
||||
|
||||
def clear_progress(self):
|
||||
"""Clear the progress text view"""
|
||||
self.progress_buffer.set_text("")
|
||||
|
||||
def show_error(self, message):
|
||||
"""Show an error dialog"""
|
||||
dialog = Gtk.MessageDialog(
|
||||
parent=self,
|
||||
flags=0,
|
||||
message_type=Gtk.MessageType.ERROR,
|
||||
buttons=Gtk.ButtonsType.OK,
|
||||
text="Error"
|
||||
)
|
||||
dialog.format_secondary_text(message)
|
||||
dialog.run()
|
||||
dialog.destroy()
|
||||
|
||||
def show_info(self, message):
|
||||
"""Show an info dialog"""
|
||||
dialog = Gtk.MessageDialog(
|
||||
parent=self,
|
||||
flags=0,
|
||||
message_type=Gtk.MessageType.INFO,
|
||||
buttons=Gtk.ButtonsType.OK,
|
||||
text="Information"
|
||||
)
|
||||
dialog.format_secondary_text(message)
|
||||
dialog.run()
|
||||
dialog.destroy()
|
||||
|
||||
def clear_current_process(self):
|
||||
"""Clear the current process reference"""
|
||||
self.current_process = None
|
||||
return False # Required for GLib.idle_add
|
||||
|
||||
def main():
|
||||
app = WormholeGUI()
|
||||
app.connect("destroy", Gtk.main_quit)
|
||||
app.show_all()
|
||||
Gtk.main()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Reference in New Issue
Block a user